Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
4ec592f
Merge origin/dev into sni-mtls-pop (resync #938 with base)
Robbie-Microsoft Jul 30, 2026
e226f92
Merge origin/dev into sni-mtls-pop
Robbie-Microsoft Oct 9, 2026
077f11f
Merge origin/dev into sni-mtls-pop
Robbie-Microsoft Oct 21, 2026
0424c53
Address mTLS PoP review comments
Robbie-Microsoft Oct 22, 2026
d3a120c
Add downstream mTLS binding context
Robbie-Microsoft Oct 22, 2026
9e35a33
Fix mTLS cache isolation and transport compatibility
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
1fceb2f
Fail required certificate E2E scenarios on missing lab configuration
Robbie-Microsoft Oct 23, 2026
f67c680
Fix certificate fixture regression when release tag is unset
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
94 changes: 94 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,100 @@ 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``).
* MSAL must own the TLS transport, so a custom ``http_client`` is not supported
together with ``mtls_proof_of_possession=True``.
Per-request ``post`` and ``cert`` overrides are also rejected.
The owned mTLS token transport never follows redirects; HTTP 3xx responses
raise ``requests.exceptions.HTTPError`` and are not cached as tokens.
* 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.

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:


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