Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
dd4f13a
Add SNI certificate support over mTLS Proof-of-Possession
Robbie-Microsoft Jul 1, 2026
81c3eb7
Use SN/I-allow-listed app for mTLS PoP e2e tests
Robbie-Microsoft Jul 8, 2026
224a003
Enumerate key-bound ATs on sign-out and account removal
Robbie-Microsoft Jul 8, 2026
02ccacb
Fail closed on mTLS PoP token_type downgrade; honor verify; lock sess…
Robbie-Microsoft Jul 8, 2026
51adcec
Isolate key/ext-bound ATs unconditionally; normalize verify=None
Robbie-Microsoft Jul 8, 2026
ba697e8
Address review comments on mTLS PoP cert/key handling
Robbie-Microsoft Jul 8, 2026
01b676b
Add FIC two-leg exchange over mTLS Proof-of-Possession
Robbie-Microsoft Jul 6, 2026
90d0e67
Fail fast when a FIC leg-2 app is used with a non-mTLS flow
Robbie-Microsoft Jul 8, 2026
acd78ed
Reject FIC leg-2 apps on silent flows too
Robbie-Microsoft Jul 8, 2026
b79166d
Harden mTLS PoP e2e: prove token at resource + naming matrix
Robbie-Microsoft Jul 30, 2026
aaf62c1
Strengthen X509->Bearer e2e: assert cert binding is absent
Robbie-Microsoft Jul 30, 2026
fec7d7f
Note _call_graph_with_mtls_pop_token is shared with FIC #939
Robbie-Microsoft Jul 30, 2026
b9233c6
Split mtls_pop/Bearer e2e by region (drop regional PoP cell)
Robbie-Microsoft Jul 30, 2026
3ce5d96
Merge SNI mTLS PoP hardening (#938) into FIC branch
Robbie-Microsoft Jul 30, 2026
4ec592f
Merge origin/dev into sni-mtls-pop (resync #938 with base)
Robbie-Microsoft Jul 30, 2026
09e8fb5
Merge resync'd #938 (origin/dev) into FIC branch #939
Robbie-Microsoft Jul 30, 2026
f98961f
Add Bearer-over-mTLS: app-level flag + client-credentials path
Robbie-Microsoft Jul 31, 2026
93faa2f
Route OBO/refresh/auth-code through Bearer-over-mTLS when flag is set
Robbie-Microsoft Jul 31, 2026
bf79927
Add Bearer-over-mTLS second-call cache-hit regression
Robbie-Microsoft Jul 31, 2026
ab024e7
Add Bearer-over-mTLS end-to-end cells
Robbie-Microsoft Jul 31, 2026
92421b0
Document the send_certificate_over_mtls client_credential flag
Robbie-Microsoft Jul 31, 2026
60a9f24
Harden Bearer-over-mTLS 2nd-call cache test with an env-lock assertion
Robbie-Microsoft Jul 31, 2026
e226f92
Merge origin/dev into sni-mtls-pop
Robbie-Microsoft Oct 9, 2026
3b45837
Merge updated sni-mtls-pop foundation
Robbie-Microsoft Oct 9, 2026
c78bb15
Merge updated sni-mtls-pop foundation
Robbie-Microsoft Oct 9, 2026
6e2c371
Address mTLS review feedback
Robbie-Microsoft Oct 21, 2026
077f11f
Merge origin/dev into sni-mtls-pop
Robbie-Microsoft Oct 21, 2026
3c66b97
Merge updated sni-mtls-pop foundation
Robbie-Microsoft Oct 21, 2026
1e41f24
Address FIC mTLS review feedback
Robbie-Microsoft Oct 22, 2026
6270370
Merge updated sni-mtls-pop foundation
Robbie-Microsoft Oct 22, 2026
a3ee807
Fix Bearer-over-mTLS transport enforcement and PFX chains
Robbie-Microsoft Oct 22, 2026
0424c53
Address mTLS PoP review comments
Robbie-Microsoft Oct 22, 2026
d3a120c
Add downstream mTLS binding context
Robbie-Microsoft Oct 22, 2026
c39abc6
Merge mTLS binding context foundation
Robbie-Microsoft Oct 22, 2026
c467604
Merge mTLS binding context foundation
Robbie-Microsoft Oct 22, 2026
9e35a33
Fix mTLS cache isolation and transport compatibility
Robbie-Microsoft Oct 23, 2026
cd043fe
Merge remote-tracking branch 'origin/rginsburg/sni-mtls-pop' into rgi…
Robbie-Microsoft Oct 23, 2026
d2b9ab2
Align FIC Bearer and PoP result binding
Robbie-Microsoft Oct 23, 2026
5818a5c
Merge mTLS PoP foundation and align Bearer semantics
Robbie-Microsoft Oct 23, 2026
28b0126
Add certificate-aware mTLS HTTP client extensibility
Robbie-Microsoft Oct 23, 2026
e574edd
Fix mTLS cache compatibility and transport review findings
Robbie-Microsoft Oct 23, 2026
6f34f2e
Avoid cache persistence reentry and quadratic migration cleanup
Robbie-Microsoft Oct 23, 2026
2de965f
Reject mixed-case raw mTLS token types before cache lookup
Robbie-Microsoft Oct 23, 2026
dd07393
Reject redirects in the owned mTLS token transport
Robbie-Microsoft Oct 23, 2026
8d4ecec
Merge PR #938 into Bearer-over-mTLS follow-up
Robbie-Microsoft Oct 23, 2026
8b703d7
Merge PR #938 into FIC mTLS follow-up
Robbie-Microsoft Oct 23, 2026
0109a28
Merge PR #938 into certificate-aware HTTP extensibility
Robbie-Microsoft Oct 23, 2026
18bfd0d
Fix custom mTLS transport response handling and dependency validation
Robbie-Microsoft Oct 23, 2026
30281ce
Fix Bearer-over-mTLS routing, certificate overrides and PFX compatibi…
Robbie-Microsoft Oct 23, 2026
b70842b
Address FIC mTLS review feedback
Robbie-Microsoft Oct 23, 2026
820c96f
Align Bearer-over-mTLS E2E failure and coverage semantics
Robbie-Microsoft Oct 23, 2026
3bc3505
Clarify default-only mTLS live E2E coverage
Robbie-Microsoft Oct 23, 2026
84ff3e8
Align FIC E2E cells with shared SDK lab setup
Robbie-Microsoft Oct 23, 2026
1fceb2f
Fail required certificate E2E scenarios on missing lab configuration
Robbie-Microsoft Oct 23, 2026
6c1a0a5
Merge shared mTLS E2E fixture validation from #938
Robbie-Microsoft Oct 23, 2026
751aa0b
Make required E2E fixture checks independent of release environment
Robbie-Microsoft Oct 23, 2026
eeb27d0
Merge required certificate E2E coverage from #938
Robbie-Microsoft Oct 23, 2026
f67c680
Fix certificate fixture regression when release tag is unset
Robbie-Microsoft Oct 23, 2026
b241b3d
Merge shared fail-closed certificate E2E fixtures
Robbie-Microsoft Oct 23, 2026
063325c
Merge shared E2E fixture portability fix
Robbie-Microsoft Oct 23, 2026
2ca4c85
Merge final shared certificate E2E fixture correction from #938
Robbie-Microsoft Oct 23, 2026
51cd6fd
Merge final shared certificate E2E correction from #938
Robbie-Microsoft Oct 23, 2026
a821098
Merge approved SNI mTLS PoP head (#938)
Robbie-Microsoft Oct 23, 2026
87b62c2
Merge approved FIC mTLS PoP head (#939)
Robbie-Microsoft Oct 23, 2026
b0cf453
Merge approved Bearer-over-mTLS head (#947)
Robbie-Microsoft Oct 23, 2026
d9f87b1
Merge approved custom mTLS HTTP head (#974)
Robbie-Microsoft Oct 23, 2026
6b0694c
Share certificate transport across mTLS flows
Robbie-Microsoft Oct 23, 2026
f5435b4
Expand required live mTLS E2E parity coverage
Robbie-Microsoft Oct 23, 2026
fea35cf
Keep mTLS sample diagnostics safe and pin fixture TLS
Robbie-Microsoft Oct 23, 2026
7f98208
Match live mTLS service representations and rejection codes
Robbie-Microsoft Oct 23, 2026
1208c07
Hide sensitive E2E helper arguments from failure tracebacks
Robbie-Microsoft Oct 23, 2026
0cd290d
Index access-token identities for bounded cache writes
Robbie-Microsoft Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .Pipelines/CI-AND-RELEASE-PIPELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ PreBuildCheck ─► UnitTests ─► E2ETests ─► Benchmark (post-merge to d
|-------|-------------|-------------|
| **PreBuildCheck** | Runs SDL security scans: PoliCheck (policy/offensive content), CredScan (leaked credentials), and PostAnalysis (breaks the build on findings) | Always |
| **UnitTests** | Runs the unit test suite on Python 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14 (no Key Vault required) | After PreBuildCheck |
| **E2ETests** | Fetches the MSID Lab certificate from Key Vault and runs `tests/test_e2e.py` + `tests/test_fmi_e2e.py` on the same Python matrix. On forked PRs the stage still runs, but the Key Vault tasks are skipped and the E2E tests self-skip (because `LAB_APP_CLIENT_CERT_PFX_PATH` is unset), so the stage reports green with all E2E tests marked Skipped in the Tests tab. | After UnitTests |
| **E2ETests** | Fetches the MSID Lab certificate from Key Vault and runs `tests/test_e2e.py` + `tests/test_fmi_e2e.py` on the same Python matrix. On forked PRs the Key Vault tasks remain skipped to protect secrets. Required certificate scenarios fail when `LAB_APP_CLIENT_CERT_PFX_PATH` is missing or invalid; missing credentials cannot produce green skipped coverage. Legacy optional skips are unchanged. | After UnitTests |
| **Benchmark** | Runs performance benchmarks on Python 3.9 and publishes `benchmark-results` artifact | Post-merge pushes to `dev` and manual runs only |

The `Validate` stage is **skipped** on PR/CI runs (it only applies to release builds).
Expand Down
8 changes: 4 additions & 4 deletions .Pipelines/template-pipeline-stages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ stages:
# certificate from Key Vault.
# Fork behaviour: the stage still runs on forked PRs, but the
# Key Vault retrieval and certificate decoding steps are skipped
# via `ne(System.PullRequest.IsFork, 'True')`. The pytest step then
# self-skips each test because LAB_APP_CLIENT_CERT_PFX_PATH is unset
# (see tests/test_e2e.py). Result: green stage on forks with all
# E2E tests reported as Skipped in the Tests tab.
# via `ne(System.PullRequest.IsFork, 'True')`. Required certificate
# scenarios then fail because LAB_APP_CLIENT_CERT_PFX_PATH is unset;
# missing credentials must not produce green skipped E2E coverage.
# Legacy optional scenarios retain their existing skips.
# ═══════════════════════════════════════════════════════════════════════════
- stage: E2ETests
displayName: 'E2E tests'
Expand Down
3 changes: 2 additions & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#################################################################################
# OneBranch Pipelines - PR Gate and CI #
# Runs on pushes to dev, on all PRs to dev, and on a daily schedule. #
# Runs on dev pushes, PRs to dev or the mTLS PoP base, and a daily schedule. #
# Documentation: https://aka.ms/obpipelines #
# Yaml Schema: https://aka.ms/obpipelines/yaml/schema #
#################################################################################
Expand All @@ -14,6 +14,7 @@ pr:
branches:
include:
- dev
- rginsburg/sni-mtls-pop
drafts: false

schedules:
Expand Down
223 changes: 223 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,229 @@ New in MSAL Python 1.26
.. automethod:: __init__


mTLS Proof-of-Possession (SN/I certificate)
-------------------------------------------

New in MSAL Python 1.38

MSAL Python supports two different kinds of Proof-of-Possession (PoP) tokens:

* **Signed HTTP Request (SHR) PoP** -- used by public-client apps through the
broker, and configured with the ``auth_scheme`` parameter and
:py:class:`msal.PopAuthScheme` above.
* **mutual-TLS (mTLS) PoP** -- used by *confidential-client* apps. The app's
Subject Name + Issuer (SN/I) certificate is presented as the **client TLS
certificate** in a mutual-TLS handshake to Microsoft Entra, and the returned
access token is cryptographically bound to that certificate
(``token_type == "mtls_pop"``, ``cnf``/``x5t#S256``).

To request an mTLS-PoP token, configure the confidential client with a
certificate credential and pass ``mtls_proof_of_possession=True`` to
:py:meth:`msal.ConfidentialClientApplication.acquire_token_for_client`::

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"
binding = result["binding_certificate"]
# binding.x5c
# binding.thumbprint_sha256

with binding.create_requests_session() as session:
response = session.get(
resource_url,
headers={
"Authorization":
"{} {}".format(
result["token_type"],
result["access_token"],
),
},
timeout=30,
)

Notes and requirements:

* The ``authority`` must be **tenanted** (not ``/common`` or ``/organizations``).
* The default HTTP transport handles mTLS automatically. A custom
``http_client`` must explicitly implement the certificate-aware factory
described below; legacy custom clients fail with ``ValueError`` for mTLS.
Per-request ``post`` and ``cert`` overrides and redirects are rejected for
mTLS token requests, including when a token could otherwise come from cache.
The built-in mTLS token transport never follows redirects; HTTP 3xx responses
raise ``requests.exceptions.HTTPError`` and are not cached as tokens.
* MSAL's built-in mTLS transport and ``binding.create_requests_session()`` require
Requests >= 2.25.0 with a compatible urllib3 that supports ``SSLContext``.
Missing transport capabilities raise ``ValueError`` with upgrade instructions.
This requirement does not change support for ordinary Bearer authentication.
* If the provider returns a different token type, MSAL returns
``token_type_mismatch`` without caching the rejected response.
* ``azure_region`` is **optional**: omission honors ``MSAL_FORCE_REGION`` and
otherwise uses the global mTLS endpoint (``mtlsauth.microsoft.com``).
Set ``azure_region=False`` to guarantee global routing, or provide a region
(e.g. ``westus3``) to pin one.
* The existing SN/I + Bearer (client-assertion) flow is unchanged; the same
certificate can be used either as an assertion signer (Bearer) or as the TLS
client certificate (mtls_pop).
* mTLS PoP currently targets the public and Azure Government (Arlington) clouds.
* Only the mTLS-PoP path drops the signed ``client_assertion``: the certificate
authenticates the client through the mutual-TLS handshake instead. The classic
(non-mTLS) certificate path is unchanged and still sends a signed
``client_assertion`` (``private_key_jwt``) to obtain an ordinary token.
* ``result["binding_certificate"]`` is a process-local
:py:class:`msal.MtlsBindingContext`. It exposes the public certificate chain
and SHA-256 thumbprint and can create an ``ssl.SSLContext`` or
``requests.Session`` that presents the same certificate to the resource. It
is not serializable and must not be persisted.
The TLS helpers accept custom CA file/directory paths as strings or
``os.PathLike`` objects (including ``pathlib.Path``); these replace the default
trust roots. ``True`` or ``None`` selects the Requests CA bundle, while
``False`` disables server verification.
* For a Federated Identity Credential (FIC) exchange, configure the leg-2 client
with both a ``client_assertion`` callable (acquiring the leg-1 token) and an
``mtls_binding_certificate`` sub-key; the leg-1 assertion is then sent as a
``jwt-pop`` client assertion over mTLS using the same certificate as leg 1.
A fixed assertion string expires and emits a deprecation warning; the callable
lets the leg-1 application reuse cached tokens and acquire fresh ones as needed.
The target application must have a federated identity credential trusting the
leg-1 token's issuer, subject, and audience. See
``fic_two_leg_over_mtls_pop()`` in
``sample/confidential_client_mtls_pop_sample.py`` for the two-leg setup.
A final ``mtls_pop`` result includes an ``MtlsBindingContext`` for the
downstream resource connection. If leg 2 does not request PoP, its bound
assertion still travels as ``jwt-pop`` over mTLS, but the final Bearer token
is not certificate-bound and does not include ``binding_certificate``.
* To request Bearer-over-mTLS instead, set
``send_certificate_over_mtls=True`` in the certificate credential and omit
``mtls_proof_of_possession``. The resulting token uses the ``Bearer`` scheme,
is not certificate-bound, and does not include ``binding_certificate``.

mTLS-bound access tokens are persisted in a separate ``AccessToken_mTLS`` cache
bucket. Older SDK versions ignore that bucket and retain their normal Bearer
and SSH behavior. Before rolling back, remove bound tokens using the newer
MSAL version or clear the entire persisted cache. An older version's
``remove_tokens_for_client()`` does not purge an unknown mTLS bucket.

.. autoclass:: msal.MtlsBindingContext
:members:

Custom HTTP pipelines for mTLS token requests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

An application that does not supply ``http_client`` needs no new configuration.
MSAL owns its certificate-bearing Requests transport, separate from discovery
and ordinary token traffic.

A custom HTTP client opts in by implementing this additional method::

def create_mtls_client(self, binding_context):
# binding_context is an MSAL-created msal.MtlsBindingContext.
# Return a NEW pipeline with the same middleware and network policy,
# but isolated certificate-bearing connections.
...

There is no marker class to inherit and no additional constructor parameter.
The ordinary client's ``get`` and ``post`` still handle discovery and non-mTLS
requests. On the first mTLS token network request, MSAL calls the factory with
the application's selected certificate. The returned object must have a
callable ``post(url, **kwargs)`` using the normal MSAL HTTP response contract
(``status_code``, ``text``, optional ``headers``, and ``raise_for_status()``).
It must accept and honor ``allow_redirects=False``. Factory and request
exceptions propagate; MSAL never falls back to the ordinary client.

The same certificate-scoped pipeline handles mTLS PoP, Bearer-over-mTLS, and
FIC leg-2 requests. A FIC assertion remains ``jwt-pop`` even when the final
token is ``Bearer``; only a final ``mtls_pop`` token includes
``binding_certificate`` in its result.

For Requests-based pipelines, mount
``binding_context.create_requests_adapter(...)`` on a **new** session. This
helper accepts ``HTTPAdapter`` options for retry and pooling policy, retains
MSAL's Requests-version compatibility handling, and selects separate
certificate-bearing TLS contexts and pools using the effective Requests
``verify`` value. Session/request CA paths, ``REQUESTS_CA_BUNDLE`` and
``CURL_CA_BUNDLE`` (when ``session.trust_env`` allows them), and HTTPS proxies
retain their Requests semantics. For other HTTP libraries,
``binding_context.create_ssl_context(verify=...)`` creates the certificate-bearing
``ssl.SSLContext``; the implementer must resolve its own library's effective
server verification and proxy settings before using it.

For example, inside a Requests-based factory::

session = MyPolicySession() # A fresh session retaining your middleware
session.verify = self.ca_bundle
session.trust_env = self.trust_env
session.proxies.update(self.proxies)
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,
))
# Set the pipeline's timeout, hooks and other policy as for ordinary traffic.
# Keep session alive until all applications using this factory have finished.
return session

The runnable ``sample/confidential_client_mtls_http_client_sample.py`` shows a
factory that retains response middleware, a header policy, CA/proxy/environment
settings, timeouts and pooling, and closes its derived pipelines explicitly.
It uses the same certificate and environment configuration as the default
mTLS-PoP sample.

Trust, isolation and lifetime contract
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

* Return a fresh pipeline for every factory call. Do not mutate or return the
ordinary client, its shared transport, or a previously returned pipeline.
Never share certificate-bearing connection pools, adapters, mutable TLS
contexts or TLS session caches with another certificate or ordinary traffic.
Shared immutable policy is fine.
* Preserve all required middleware and networking policy explicitly: MSAL cannot
clone an opaque client. Constructor ``verify``, ``proxies`` and ``timeout``
options still do not override a custom HTTP client's settings.
* MSAL serializes factory creation within each application and caches one
returned pipeline for that application's certificate. Global and regional
token endpoints reuse it. Separate applications invoke the factory separately,
even when they share an HTTP factory or caches. The factory must be safe to
call concurrently from different applications, and its returned pipelines
must support the application's concurrency requirements.
* Certificate credentials are fixed for the application's lifetime. To rotate,
create a new application and pipeline, stop in-flight work using the old
application, then release its pipelines. Do not mutate a supplied binding
context or a TLS context already used by a connection pool.
* The custom factory owns and must retain/close every client it creates,
including partial constructions when its own code raises. MSAL does not
close caller-owned clients or the ordinary pipeline. Close them only after
all applications and in-flight requests using them have finished.
* MSAL rejects a missing/non-callable factory, a return value without callable
``post``, returning the ordinary client itself, and returning a client already
assigned to another live application. This identity check does not keep
applications alive or require custom clients to support weak references.
MSAL restricts its token calls to the configured HTTPS endpoint, passes
``allow_redirects=False``,
and rejects redirect responses or observable redirect history/final-endpoint
changes (excluding query parameters). These checks cannot undo a redirect
already followed by a faulty custom implementation.
* This is a **trusted implementation contract**, not proof that arbitrary code
uses the supplied certificate. MSAL cannot inspect nested transports, detect
all shared pools (or all reuse after an application is collected), enforce an
opaque client's verification/proxy policy, or prevent custom middleware from
ignoring TLS or redirect instructions. The implementer is responsible for
those behaviors; do not opt in unless the complete pipeline honors them.

The factory receives a process-local certificate capability, not a successful
token result. ``binding_certificate`` remains present only on a final
``mtls_pop`` result, never on an ordinary Bearer result. This extension does not
change downstream resource TLS or the requirements for mTLS PoP.


Exceptions
----------
These are exceptions that MSAL Python may raise.
Expand Down
2 changes: 1 addition & 1 deletion msal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .sku import __version__
from .token_cache import TokenCache, SerializableTokenCache
from .auth_scheme import PopAuthScheme
from .mtls import MtlsBindingContext
from .managed_identity import (
SystemAssignedManagedIdentity, UserAssignedManagedIdentity,
ManagedIdentityClient,
Expand All @@ -46,4 +47,3 @@
# 1. officially part of the MSAL public API, and
# 2. can still be caught by the user code even if we change the module structure.
from .oauth2cli.oauth2 import BrowserInteractionTimeoutError

Loading
Loading