Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
3b45837
Merge updated sni-mtls-pop foundation
Robbie-Microsoft Oct 9, 2026
6e2c371
Address mTLS review feedback
Robbie-Microsoft Oct 21, 2026
3c66b97
Merge updated sni-mtls-pop foundation
Robbie-Microsoft Oct 21, 2026
a3ee807
Fix Bearer-over-mTLS transport enforcement and PFX chains
Robbie-Microsoft Oct 22, 2026
c467604
Merge mTLS binding context foundation
Robbie-Microsoft Oct 22, 2026
5818a5c
Merge mTLS PoP foundation and align Bearer semantics
Robbie-Microsoft Oct 23, 2026
8d4ecec
Merge PR #938 into Bearer-over-mTLS follow-up
Robbie-Microsoft Oct 23, 2026
30281ce
Fix Bearer-over-mTLS routing, certificate overrides and PFX compatibi…
Robbie-Microsoft Oct 23, 2026
820c96f
Align Bearer-over-mTLS E2E failure and coverage semantics
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
f481380
Relocate default Bearer-over-mTLS E2E coverage
Robbie-Microsoft Oct 27, 2026
5b0c827
Merge core mTLS parity fixtures and cache fix into Bearer coverage
Robbie-Microsoft Oct 27, 2026
7692d10
Merge compatible mTLS E2E transport cleanup
Robbie-Microsoft Oct 27, 2026
8e6acee
Treat an unspecified certificate consistently across mTLS requests
Robbie-Microsoft Oct 27, 2026
37d8099
Merge sanitized mTLS transport failure boundaries
Robbie-Microsoft Oct 27, 2026
4ec4536
Reject real Bearer certificate overrides before cache lookup
Robbie-Microsoft Oct 27, 2026
873937b
Merge recorder validation privacy and duplicate-removal fixes
Robbie-Microsoft Oct 27, 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
4 changes: 4 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ Notes and requirements:
``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.
* 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
Expand Down
192 changes: 180 additions & 12 deletions msal/application.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions msal/mtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ def get(self, url, **kwargs):
def _request(self, method, url, **kwargs):
from requests.exceptions import HTTPError

if kwargs.get("cert") is not None:
raise ValueError(
"MSAL's mTLS transport does not support a cert override. "
"Use the configured client certificate.")
if self._timeout is not None:
kwargs.setdefault("timeout", self._timeout)
# The certificate and token request must stay at the selected authority.
Expand Down
54 changes: 40 additions & 14 deletions sample/confidential_client_mtls_pop_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,35 @@ def _print_result_summary(result, header):
print(header)
if "access_token" in result:
print(" acquired: yes")
print(" token_type is mtls_pop: ", result.get("token_type") == "mtls_pop")
print(" token_type is mtls_pop: ", result.get("token_type") == "mtls_pop")
print(" carries binding_certificate: ", "binding_certificate" in result)
print(" served from cache: ", result.get("token_source") == "cache")
else:
print(" acquired: no")
print(" (inspect result['error'] / result['error_description'] to diagnose)")


def _call_resource(result):
resource_url = os.getenv("RESOURCE_URL")
if not resource_url or "access_token" not in result:
return
binding = result["binding_certificate"]
with binding.create_requests_session() as session:
response = session.get(
resource_url,
headers={
"Authorization":
"{} {}".format(
result["token_type"],
result["access_token"],
),
},
timeout=30,
)
response.raise_for_status()
print(" resource call status: ", response.status_code)


def vanilla_sni_mtls_pop():
"""Acquire an mTLS-bound PoP token using the SN/I cert as the TLS client cert."""
app = msal.ConfidentialClientApplication(
Expand All @@ -94,22 +115,27 @@ def vanilla_sni_mtls_pop():
)

_print_result_summary(result, "Vanilla SN/I -> mTLS PoP result:")
resource_url = os.getenv("RESOURCE_URL")
if resource_url and "access_token" in result:
binding = result["binding_certificate"]
with binding.create_requests_session() as session:
response = session.get(
resource_url,
headers={
"Authorization": "mtls_pop " + result["access_token"],
},
timeout=30,
)
response.raise_for_status()
print(" resource call status: ", response.status_code)
_call_resource(result)
return result


def bearer_over_mtls():
"""Acquire an ordinary Bearer token while authenticating over mTLS."""
credential = _build_cert_credential()
credential["send_certificate_over_mtls"] = True
app = msal.ConfidentialClientApplication(
os.environ["CLIENT_ID"],
authority=os.environ["AUTHORITY"],
client_credential=credential,
azure_region=os.getenv("AZURE_REGION"),
)
result = app.acquire_token_for_client(os.environ["SCOPE"].split())
_print_result_summary(result, "SN/I -> Bearer-over-mTLS result:")
return result


if __name__ == "__main__":
print("=== Vanilla SN/I -> mTLS PoP ===")
vanilla_sni_mtls_pop()
print("\n=== SN/I -> Bearer-over-mTLS ===")
bearer_over_mtls()
37 changes: 31 additions & 6 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,21 @@ Run only the required certificate scenarios:
python -m pytest -q tests/test_e2e.py::CertificateScenariosTestCase
```

The six required certificate methods use the MSID Lab certificate and the
The eleven required certificate methods use the MSID Lab certificate and the
preconfigured client `163ffef9-a313-45b4-ab2f-c7e2f5e0e23e` in tenant
`bea21ebe-8b64-4d06-9f6d-6a889b120a7c`, matching the .NET and Go lab setup.

Bearer-over-mTLS also has a live client-credentials case for `westus3` and
`https://vault.azure.net/.default`. OBO, refresh-token, and authorization-code
captures delegate to the shipped TLS transport and require real service
responses, verified signed client assertions, grant-specific fields, and exact
OAuth rejection/status checks. The OBO flag-off control requires the ordinary
endpoint. Transport, credential, and configuration failures cannot pass as
successful captures. These deliberately invalid grants do not prove successful
user-token issuance; the reference SDKs also leave those acquisitions disabled.
They do not require fetching unrelated user configuration from Key Vault.
Legacy interactive and other optional E2E skips are unchanged.

Global Graph PoP and regional `westus3` Key Vault PoP are separate cases.
Ordinary regional signed-certificate Bearer authentication is not
Bearer-over-mTLS: it uses the ordinary endpoint and retains the central
Expand All @@ -81,15 +93,15 @@ adapter, and verify signed certificate assertions against the certificate,
client, and expected audience. They retain bounded status/error metadata,
never raw forms, tokens, private keys, or client assertions.

All six methods run without skips, including tagged releases. Missing
All eleven methods run without skips, including tagged releases. Missing
configuration, credential failures, and DNS/TLS failures are failures, not
acceptable rejection evidence. They do not fetch unrelated user configuration
from Key Vault. Legacy interactive and other optional E2E skips are unchanged.

Offline guards are separate from live evidence:

```powershell
python -m pytest -q tests\test_lab_config.py tests\test_mtls_e2e_helpers.py tests\test_token_cache.py
python -m pytest -q tests\test_lab_config.py tests\test_mtls_e2e_helpers.py tests\test_bearer_mtls_e2e_helpers.py tests\test_token_cache.py
```

These checks use local fixtures or synthetic responses to verify strict
Expand All @@ -107,7 +119,7 @@ and linear population work while preserving legacy renewal and persistence.
Native removal must delete equal canonical and legacy copies without a reload,
while preserving renewed replacements and other certificates.

### Pinned core certificate parity mapping
### Pinned certificate and Bearer transport parity mapping

The applicable active reference cases below are pinned:

Expand All @@ -125,14 +137,27 @@ The applicable active reference cases below are pinned:
| Go `TestCredential_X509_Output_Pop_Regional` | `C.test_credential_x509_output_pop_regional` | westus3 Key Vault PoP, certificate/JWT binding, same cached token and type; not a redirected global Graph cell. |
| Go `TestConfidentialClientSNIBearerAndMtlsPoPCacheIsolated` | `C.test_bearer_then_mtls_pop_cache_isolated` | One app/cache/scope, Bearer then PoP, distinct tokens and schemes, no Bearer binding, both cache results preserved. |
| Go `TestConfidentialClientSNIMtlsPoPThenBearerCacheIsolated` | `C.test_mtls_pop_then_bearer_cache_isolated` | Reverse acquisition order with the same isolation and reusable PoP result. |
| .NET live client-credentials Bearer-over-mTLS | `C.test_credential_x509_send_certificate_over_mtls_output_bearer` | westus3 Key Vault Bearer over the real mTLS transport, verified signed assertion, no output binding, cached repeat. |
| Go/.NET active OBO recording | `C.test_send_certificate_over_mtls_on_behalf_of_request_capture` | Actual mTLS POST, signed assertion and grant fields; HTTP 400, `invalid_request`, AADSTS50027. |
| .NET active refresh recording | `C.test_send_certificate_over_mtls_refresh_token_request_capture` | Actual mTLS POST with the refresh field; HTTP 400 and `invalid_grant`. |
| Go/.NET active authorization-code recording | `C.test_send_certificate_over_mtls_auth_code_request_capture` | Actual mTLS POST with code/redirect fields; HTTP 400 and `invalid_grant`. |
| .NET flag-off OBO control | `C.test_without_send_certificate_over_mtls_on_behalf_of_regular_endpoint` | Ordinary endpoint and adapter, signed assertion, no PoP fields; exact malformed-OBO rejection. |

`C.test_subject_name_issuer_authentication` remains an additional required
legacy SN/I lab cell. FIC, Bearer-over-mTLS, and custom HTTP pipeline scenarios
belong to their feature PRs, not this core inventory. This suite does not
legacy SN/I lab cell. FIC and custom HTTP pipeline scenarios
belong to their feature PRs, not this inventory. This suite does not
provision apps or claim repository-wide skip elimination.

The Bearer transport references are the pinned .NET
[transport cases][dotnet-transport] and Go
`903eb9f1e54e361525025ea27040ac9644b64358` [Bearer cases][go-bearer].
The offline rejection guard explicitly rejects token success, invalid-client
errors, wrong statuses and wrong AADSTS codes as capture evidence.

[dotnet-cc]: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/c8b90e9de5bc4974de9928835532faac5c88e2d2/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs
[go-pop]: https://github.com/AzureAD/microsoft-authentication-library-for-go/blob/903eb9f1e54e361525025ea27040ac9644b64358/apps/tests/integration/mtls_pop_integration_test.go
[dotnet-transport]: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/c8b90e9de5bc4974de9928835532faac5c88e2d2/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/MtlsTransportUserFlowTests.cs
[go-bearer]: https://github.com/AzureAD/microsoft-authentication-library-for-go/blob/903eb9f1e54e361525025ea27040ac9644b64358/apps/tests/integration/bearer_mtls_integration_test.go

## 5) Manual-intervention E2E tests

Expand Down
Loading
Loading