Skip to content

feat: extend mTLS bearer transport (SendCertificateOverMtls) to OBO, refresh_token, and auth_code flows#6009

Merged
Robbie-Microsoft merged 30 commits into
mainfrom
rginsburg/mtls_bearer_user_flows
Jun 4, 2026
Merged

feat: extend mTLS bearer transport (SendCertificateOverMtls) to OBO, refresh_token, and auth_code flows#6009
Robbie-Microsoft merged 30 commits into
mainfrom
rginsburg/mtls_bearer_user_flows

Conversation

@Robbie-Microsoft

@Robbie-Microsoft Robbie-Microsoft commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

When SendCertificateOverMtls=true, MSAL previously only applied mTLS bearer transport for AcquireTokenForClient. User flows (OBO, refresh_token, auth_code) continued to use the regular login.microsoftonline.com endpoint with a client_assertion JWT in the POST body.

This PR extends mTLS bearer transport to all three user flows so they behave consistently with client_credentials when mTLS transport is configured.

Root Cause

TryInitMtlsPopParametersAsync (which sets MtlsCertificate on the request parameters, triggering mTLS endpoint routing) was only called in ConfidentialClientExecutor.ExecuteAsync for AcquireTokenForClientParameters. The OBO, auth_code, and RT executor paths skipped it entirely.

Changes

Production

File Change
ConfidentialClientExecutor.cs Add TryInitMtlsPopParametersAsync to OBO and auth_code executor paths
ClientApplicationBaseExecutor.cs Add TryInitMtlsPopParametersAsync to the refresh_token (IByRefreshToken) and AcquireTokenSilent paths
RegionAndMtlsDiscoveryProvider.cs Attempt region discovery for mTLS user flows when WithAzureRegion is configured, so regional endpoints (e.g. eastus.mtlsauth.microsoft.com) are used
TokenCache.ITokenCacheInternal.cs Use OriginalAuthority for cache alias resolution so mTLS-transformed (mtlsauth.*) endpoints do not crash on second call
MtlsPopParametersInitializer.cs Case 2 (IClientSignedAssertionProvider.GetAssertionAsync) is not guarded by SendCertificateOverMtls — it is a separate opt-in where the delegate signals mTLS intent by returning a non-null TokenBindingCertificate. This is independent of Case 1 (SendCertificateOverMtls + cert-based credential).
CredentialMaterialResolver.cs Mode=Mtls only when IsMtlsPopRequested (explicit PoP); Mode=OAuth for all bearer transport cases so client_assertion is included in the body for every flow. Also auto-enables SendX5C=true when SendCertificateOverMtls=true — required for SNI-registered apps to validate the assertion JWT via the x5c chain.
AcquireTokenParameterBuilderExtensions.cs, AcquireTokenCommonParameters.cs, TokenClient.cs, AuthenticationRequestParameters.cs, PublicAPI.Unshipped.txt (×6) Restored WithCachePartitionKey, WithReservedScopes, and SendOfflineAccessScope lost during a prior merge conflict resolution (from PR #6014 / 022dcde32)

Tests

File Description
MtlsBearerUserFlowTests.cs (new) Unit tests: OBO global mTLS, RT global mTLS, regional mTLS routing (full OBO post-data assertions including grant_type and requested_token_use), regression for non-mTLS cert credential, regression for second-call cache crash
MtlsTransportUserFlowTests.cs (new) Integration tests: OboFlow/RefreshTokenFlow/AuthCodeFlow/ClientCredentials_WithSendCertificateOverMtls_BothMtlsConditionsMet, OboFlow_WithoutSendCertificateOverMtls_UsesRegularEndpointAsync. OboFlow/RefreshTokenFlow_WithSendCertificateOverMtls_AcquiresTokenAsync are [Ignore]'d pending lab config (see Testing section).

Docs

File Description
docs/mtls-bearer-transport.md (new) User-facing guide: opt-in API, IMsalMtlsHttpClientFactory implementation example, supported flows, how to verify, AAD allowlisting requirement

Design: Preview Behavior

For this preview drop, all flows send client_assertion in the POST body and present the cert at the TLS layer when SendCertificateOverMtls=true. This matches what ESTS supports today across all grant types.

Flow Endpoint client_assertion in body
AcquireTokenForClient (S2S) mtlsauth.microsoft.com Yes
OBO mtlsauth.microsoft.com Yes
refresh_token mtlsauth.microsoft.com Yes
auth_code mtlsauth.microsoft.com Yes

"Cert-only" (no client_assertion) is a future ESTS change, deferred post-preview.

Bug Fixes (found during review)

Bug 1 - Cache crash on second mTLS call:
TokenCache.ITokenCacheInternal.cs used requestParams.AuthorityInfo (a live property that returns the mTLS-transformed mtlsauth.* host after ResolveAuthorityAsync) for cache alias resolution. RegionAndMtlsDiscoveryProvider throws MtlsPopNotSupportedForEnvironment for non-login.* hosts. First call was safe (empty cache, early return); second call had cached entries and crashed. Fixed by using requestParams.AuthorityManager.OriginalAuthority.AuthorityInfo.

Bug 2 - AcquireTokenSilent not routing to mTLS endpoint:
ClientApplicationBaseExecutor.ExecuteAsync(AcquireTokenSilentParameters) never called TryInitMtlsPopParametersAsync, so silent refresh-token redemptions always hit login.microsoftonline.com even when SendCertificateOverMtls=true.

Bug 3 - SNI regression: missing x5c in client_assertion:
Changing CredentialMaterialResolver.cs to Mode=OAuth for all non-PoP requests caused CertificateAndClaimsClientCredential to start sending a client_assertion JWT where previously none was sent. For SNI-registered apps, AAD requires the x5c chain in the JWT header to validate the assertion. Without x5c, AADSTS700027 is returned. Fixed by auto-enabling SendX5C=true when SendCertificateOverMtls=true.

Bug 4 - TokenBindingCertificate unit tests broken by overly-broad guard:
An earlier review comment suggested guarding Case 2 in MtlsPopParametersInitializer with SendCertificateOverMtls == true. This broke BearerClientAssertion_WithPoPDelegate_Works and 3 related tests because Case 2 is a separate opt-in mechanism that must fire regardless of SendCertificateOverMtls. The guard was removed.

Testing

Unit Tests (net8.0): 2,063 passed, 0 failed

Integration Tests:

  • Sni_Over_Mtls_Gets_Bearer_Token_Successfully_TestAsync — passes (pre-existing test, fixed by Bug 3 fix above)
  • ClientCredentials_WithSendCertificateOverMtls_BothMtlsConditionsMet — passes (MSI-allowlisted app, 163ffef9)
  • OboFlow_WithSendCertificateOverMtls_BothMtlsConditionsMet — passes (recording factory)
  • RefreshTokenFlow_WithSendCertificateOverMtls_BothMtlsConditionsMet — passes (recording factory)
  • AuthCodeFlow_WithSendCertificateOverMtls_BothMtlsConditionsMetAsync — passes (fake auth code, recording factory)
  • OboFlow_WithoutSendCertificateOverMtls_UsesRegularEndpointAsync — passes
  • OboFlow_WithSendCertificateOverMtls_AcquiresTokenAsync[Ignore] pending lab config: AppWebApi (23c64cd8) mTLS not yet enabled in ID4SLAB1 (AADSTS700027). Remove [Ignore] once Bogdan/Qi enable mTLS client auth.
  • RefreshTokenFlow_WithSendCertificateOverMtls_AcquiresTokenAsync[Ignore] pending lab config: AppS2S mTLS endpoint not yet configured for this scenario (AADSTS392189). Remove [Ignore] once Bogdan/Qi enable mTLS client auth.

Existing MtlsPopTests suite: 69/69 pass.

…de flows

When SendCertificateOverMtls=true, MSAL previously only routed
AcquireTokenForClient to the mTLS endpoint (mtlsauth.microsoft.com) and
suppressed client_assertion from the POST body. User flows (OBO,
refresh_token, auth_code) fell through to the regular login endpoint with
a client_assertion JWT.

This change extends the feature to all three user flows by calling
TryInitMtlsPopParametersAsync in each executor path, mirroring the existing
AcquireTokenForClient behaviour.

Changes:
- ConfidentialClientExecutor: add TryInitMtlsPopParametersAsync to OBO and
  auth_code executor paths
- ClientApplicationBaseExecutor: add TryInitMtlsPopParametersAsync to the
  refresh_token (IByRefreshToken) executor path
- RegionAndMtlsDiscoveryProvider: attempt region discovery for mTLS-enabled
  user flows when the app has configured WithAzureRegion, so regional mTLS
  endpoints (e.g. eastus.mtlsauth.microsoft.com) are used for OBO/RT
- TokenCache: use OriginalAuthority for cache alias resolution so that
  mTLS-transformed (mtlsauth.*) endpoints do not propagate into cache lookups

Tests:
- MtlsBearerUserFlowTests.cs: 4 unit tests (OBO global/regional mTLS, RT
  global mTLS, regression for non-mTLS cert credential)
- MtlsTransportUserFlowTests.cs: updated integration tests asserting both mTLS
  transport conditions (mtlsauth endpoint + no client_assertion) for OBO and RT

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 14, 2026 17:28
@Robbie-Microsoft Robbie-Microsoft requested a review from a team as a code owner May 14, 2026 17:28

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

Note

Copilot was unable to run its full agentic suite in this review.

Extends mTLS bearer transport behavior (SendCertificateOverMtls) beyond client_credentials to OBO, refresh_token, and auth_code flows, ensuring mTLS endpoint routing and suppressing client_assertion where appropriate.

Changes:

  • Initialize mTLS/PoP parameters for auth_code, OBO, and refresh_token executor paths.
  • Enable region discovery for mTLS-enabled user flows when AzureRegion is configured (regional mtlsauth endpoints).
  • Prevent mTLS-transformed authorities from leaking into token cache alias resolution by using OriginalAuthority.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsBearerUserFlowTests.cs Adds unit coverage validating mtlsauth routing + client_assertion suppression for OBO/RT, plus a non-mTLS regression case
tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/MtlsTransportUserFlowTests.cs Adds/updates integration coverage for mTLS transport factory usage and asserts endpoint/body conditions for OBO/RT/client_credentials
src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs Uses OriginalAuthority for alias resolution to avoid mtlsauth host affecting cache lookups
src/client/Microsoft.Identity.Client/Instance/Discovery/RegionAndMtlsDiscoveryProvider.cs Attempts region discovery for mTLS user flows when AzureRegion is configured
src/client/Microsoft.Identity.Client/ApiConfig/Executors/ConfidentialClientExecutor.cs Calls TryInitMtlsPopParametersAsync for auth_code and OBO paths
src/client/Microsoft.Identity.Client/ApiConfig/Executors/ClientApplicationBaseExecutor.cs Calls TryInitMtlsPopParametersAsync for refresh_token path

Robbie-Microsoft and others added 2 commits May 14, 2026 14:16
CertHelper.GetOrCreateTestCert() returns a static cached instance.
Calling Dispose() in ClassCleanup poisons the cache, causing
MtlsPopTests.ClassInitialize to receive a disposed X509Certificate2
(m_safeCertContext is an invalid handle) when it runs alphabetically
after MtlsBearerUserFlowTests.

CertHelper owns the certificate lifetime; test classes must not
dispose certs obtained from it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Clear MSAL_FORCE_REGION in regional unit test for defensive isolation
- Clarify assertion messages to specify GetHttpClient(X509Certificate2) overload

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 14, 2026 19:18

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 6 out of 6 changed files in this pull request and generated 5 comments.

Comment thread tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsBearerUserFlowTests.cs Outdated
- Fix 'requestwith' typo in XML doc
- Clarify that ExpectedPostData checks client_assertion_type (not the
  client_assertion value itself, which is a dynamically generated JWT)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsBearerUserFlowTests.cs Outdated

@bgavrilMS bgavrilMS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No integration tests.

…Flow test, no Console.WriteLine

- MtlsTransportUserFlowTests: replace secret-based OBO/RT factory tests with
  cert+SendCertificateOverMtls=true (OboFlow_WithSendCertificateOverMtls_AcquiresTokenAsync,
  RefreshTokenFlow_WithSendCertificateOverMtls_AcquiresTokenAsync), making them true mTLS
  integration tests that assert on both the mTLS endpoint and factory invocation
- Remove SilentFlow_WithMtlsTransportFactory_UsesRefreshTokenOverMtlsAsync: it attached
  IMsalMtlsHttpClientFactory to a PCA (public client), which does not perform cert-based
  client authentication; the test did not exercise the feature being changed
- Remove _oboClientSecret, _keyVault, and secret-based TestInitialize; credentials are
  now the lab cert via SendCertificateOverMtls across all tests
- Remove all Console.WriteLine calls; diagnostic context is embedded in Assert messages
- MtlsBearerUserFlowTests: rename regional unit test to
  UserFlow_WithSendCertificateOverMtls_WithRegion_UsesRegionalMtlsEndpointAsync and clarify
  in XML doc that it is a general-purpose regional routing test (shared code path across all
  user flows), not an OBO-specific test
Copilot AI review requested due to automatic review settings May 19, 2026 15:40

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 6 out of 6 changed files in this pull request and generated 1 comment.

…est matrix

Adds the missing (OBO × client_secret) cell to the 2x2 grant/auth-mechanism matrix:

  | grant             | auth mechanism       | expected |
  |-------------------|----------------------|----------|
  | client_credentials| mTLS (no assertion)  | PASS     |
  | client_credentials| client_secret        | PASS     |
  | OBO               | mTLS (no assertion)  | FAIL*    |
  | OBO               | client_secret        | PASS ← new |

* Fails with AADSTS51000: MtlsClientAuth is/are disabled on AppWebApi.
  The baseline test proves OBO itself works; the mTLS failure is app-config only.

Also updates OboFlow_WithSendCertificateOverMtls_AcquiresTokenAsync XML doc to
cross-reference the 2x2 matrix and clarify expected failure reason.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Robbie-Microsoft Robbie-Microsoft marked this pull request as draft May 20, 2026 19:30
Bug #1 (cache crash on 2nd mTLS call):
- TokenCache.ITokenCacheInternal.cs: FilterTokensByEnvironmentAsync and
  FindRefreshTokenAsync used requestParams.AuthorityInfo for alias resolution.
  After ResolveAuthorityAsync(), AuthorityInfo.Host is 'mtlsauth.microsoft.com',
  which causes RegionAndMtlsDiscoveryProvider to throw MtlsPopNotSupportedForEnvironment.
- Fix: use requestParams.AuthorityManager.OriginalAuthority.AuthorityInfo (same
  pattern already applied to GetTenantProfilesAsync in this file).
- Added regression test: OboFlow_WithSendCertificateOverMtls_SecondCallDoesNotCrashAsync

Bug #2 (AcquireTokenSilent does not route RT redemption to mTLS endpoint):
- ClientApplicationBaseExecutor.cs: the AcquireTokenSilentParameters overload
  never called TryInitMtlsPopParametersAsync, so IsMtlsRequested=false and
  RT redemption went to login.microsoftonline.com instead of mtlsauth.microsoft.com.
- Fix: add TryInitMtlsPopParametersAsync call before CreateRequestContextAndLogVersionInfo.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 21, 2026 14:01
Copilot AI review requested due to automatic review settings June 4, 2026 16:25

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 11 out of 11 changed files in this pull request and generated no new comments.

@Robbie-Microsoft Robbie-Microsoft enabled auto-merge (squash) June 4, 2026 16:51
@Robbie-Microsoft Robbie-Microsoft merged commit bad7cae into main Jun 4, 2026
15 checks passed
@Robbie-Microsoft Robbie-Microsoft deleted the rginsburg/mtls_bearer_user_flows branch June 4, 2026 19:26
This was referenced Jun 5, 2026
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.

5 participants