feat: extend mTLS bearer transport (SendCertificateOverMtls) to OBO, refresh_token, and auth_code flows#6009
Merged
Merged
Conversation
…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>
Contributor
There was a problem hiding this comment.
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 |
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>
- 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>
bgavrilMS
approved these changes
May 18, 2026
bgavrilMS
reviewed
May 18, 2026
bgavrilMS
reviewed
May 18, 2026
bgavrilMS
reviewed
May 18, 2026
bgavrilMS
reviewed
May 18, 2026
bgavrilMS
reviewed
May 18, 2026
…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
…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>
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>
trwalke
approved these changes
Jun 4, 2026
This was referenced Jun 5, 2026
Open
Open
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When
SendCertificateOverMtls=true, MSAL previously only applied mTLS bearer transport forAcquireTokenForClient. User flows (OBO, refresh_token, auth_code) continued to use the regularlogin.microsoftonline.comendpoint with aclient_assertionJWT in the POST body.This PR extends mTLS bearer transport to all three user flows so they behave consistently with
client_credentialswhen mTLS transport is configured.Root Cause
TryInitMtlsPopParametersAsync(which setsMtlsCertificateon the request parameters, triggering mTLS endpoint routing) was only called inConfidentialClientExecutor.ExecuteAsyncforAcquireTokenForClientParameters. The OBO, auth_code, and RT executor paths skipped it entirely.Changes
Production
ConfidentialClientExecutor.csTryInitMtlsPopParametersAsyncto OBO and auth_code executor pathsClientApplicationBaseExecutor.csTryInitMtlsPopParametersAsyncto the refresh_token (IByRefreshToken) andAcquireTokenSilentpathsRegionAndMtlsDiscoveryProvider.csWithAzureRegionis configured, so regional endpoints (e.g.eastus.mtlsauth.microsoft.com) are usedTokenCache.ITokenCacheInternal.csOriginalAuthorityfor cache alias resolution so mTLS-transformed (mtlsauth.*) endpoints do not crash on second callMtlsPopParametersInitializer.csIClientSignedAssertionProvider.GetAssertionAsync) is not guarded bySendCertificateOverMtls— it is a separate opt-in where the delegate signals mTLS intent by returning a non-nullTokenBindingCertificate. This is independent of Case 1 (SendCertificateOverMtls+ cert-based credential).CredentialMaterialResolver.csMode=Mtlsonly whenIsMtlsPopRequested(explicit PoP);Mode=OAuthfor all bearer transport cases soclient_assertionis included in the body for every flow. Also auto-enablesSendX5C=truewhenSendCertificateOverMtls=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)WithCachePartitionKey,WithReservedScopes, andSendOfflineAccessScopelost during a prior merge conflict resolution (from PR #6014 /022dcde32)Tests
MtlsBearerUserFlowTests.cs(new)grant_typeandrequested_token_use), regression for non-mTLS cert credential, regression for second-call cache crashMtlsTransportUserFlowTests.cs(new)OboFlow/RefreshTokenFlow/AuthCodeFlow/ClientCredentials_WithSendCertificateOverMtls_BothMtlsConditionsMet,OboFlow_WithoutSendCertificateOverMtls_UsesRegularEndpointAsync.OboFlow/RefreshTokenFlow_WithSendCertificateOverMtls_AcquiresTokenAsyncare[Ignore]'d pending lab config (see Testing section).Docs
docs/mtls-bearer-transport.md(new)IMsalMtlsHttpClientFactoryimplementation example, supported flows, how to verify, AAD allowlisting requirementDesign: Preview Behavior
For this preview drop, all flows send
client_assertionin the POST body and present the cert at the TLS layer whenSendCertificateOverMtls=true. This matches what ESTS supports today across all grant types.client_assertionin bodyAcquireTokenForClient(S2S)mtlsauth.microsoft.commtlsauth.microsoft.commtlsauth.microsoft.commtlsauth.microsoft.com"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.csusedrequestParams.AuthorityInfo(a live property that returns the mTLS-transformedmtlsauth.*host afterResolveAuthorityAsync) for cache alias resolution.RegionAndMtlsDiscoveryProviderthrowsMtlsPopNotSupportedForEnvironmentfor non-login.*hosts. First call was safe (empty cache, early return); second call had cached entries and crashed. Fixed by usingrequestParams.AuthorityManager.OriginalAuthority.AuthorityInfo.Bug 2 -
AcquireTokenSilentnot routing to mTLS endpoint:ClientApplicationBaseExecutor.ExecuteAsync(AcquireTokenSilentParameters)never calledTryInitMtlsPopParametersAsync, so silent refresh-token redemptions always hitlogin.microsoftonline.comeven whenSendCertificateOverMtls=true.Bug 3 - SNI regression: missing x5c in
client_assertion:Changing
CredentialMaterialResolver.cstoMode=OAuthfor all non-PoP requests causedCertificateAndClaimsClientCredentialto start sending aclient_assertionJWT where previously none was sent. For SNI-registered apps, AAD requires the x5c chain in the JWT header to validate the assertion. Without x5c,AADSTS700027is returned. Fixed by auto-enablingSendX5C=truewhenSendCertificateOverMtls=true.Bug 4 -
TokenBindingCertificateunit tests broken by overly-broad guard:An earlier review comment suggested guarding Case 2 in
MtlsPopParametersInitializerwithSendCertificateOverMtls == true. This brokeBearerClientAssertion_WithPoPDelegate_Worksand 3 related tests because Case 2 is a separate opt-in mechanism that must fire regardless ofSendCertificateOverMtls. 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— passesOboFlow_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:AppS2SmTLS endpoint not yet configured for this scenario (AADSTS392189). Remove[Ignore]once Bogdan/Qi enable mTLS client auth.Existing
MtlsPopTestssuite: 69/69 pass.