Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -193,20 +193,6 @@ internal override Task<AuthenticationResult> ExecuteInternalAsync(CancellationTo
/// <seealso cref="ConfidentialClientApplicationBuilder.Validate"/> for a comment inside this function for AzureRegion.
protected override void Validate()
{
if (CommonParameters.MtlsCertificate != null)
{
// Check for Azure region only if the authority is AAD
// AzureRegion is by default set to null or set to null when the application is created
// with region set to DisableForceRegion (see ConfidentialClientApplicationBuilder.Validate)
if (ServiceBundle.Config.Authority.AuthorityInfo.AuthorityType == AuthorityType.Aad &&
ServiceBundle.Config.AzureRegion == null)
{
throw new MsalClientException(
MsalError.MtlsPopWithoutRegion,
MsalErrorMessage.MtlsPopWithoutRegion);
}
}

base.Validate();

// Force refresh + AccessTokenHashToRefresh APIs cannot be used together
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ private static async Task TryInitImplicitBearerOverMtlsAsync(
{
if (tokenParameters.MtlsCertificate != null)
{
ThrowIfRegionMissingForImplicitMtls(serviceBundle);
return;
}

Expand All @@ -59,7 +58,6 @@ private static async Task TryInitImplicitBearerOverMtlsAsync(
if (ar?.TokenBindingCertificate != null)
{
tokenParameters.MtlsCertificate = ar.TokenBindingCertificate;
ThrowIfRegionMissingForImplicitMtls(serviceBundle);
}
}
}
Expand Down Expand Up @@ -145,29 +143,11 @@ private static void InitMtlsPopParameters(
MsalError.MissingTenantedAuthority,
MsalErrorMessage.MtlsNonTenantedAuthorityNotAllowedMessage);
}

if (serviceBundle.Config.AzureRegion == null)
{
throw new MsalClientException(
MsalError.MtlsPopWithoutRegion,
MsalErrorMessage.MtlsPopWithoutRegion);
}
}

p.AuthenticationOperation = new MtlsPopAuthenticationOperation(cert);
p.MtlsCertificate = cert;
}

private static void ThrowIfRegionMissingForImplicitMtls(IServiceBundle serviceBundle)
{
// Implicit bearer-over-mTLS requires region only for AAD
if (serviceBundle.Config.Authority.AuthorityInfo.AuthorityType == AuthorityType.Aad &&
serviceBundle.Config.AzureRegion == null)
{
throw new MsalClientException(
MsalError.MtlsBearerWithoutRegion,
MsalErrorMessage.MtlsBearerWithoutRegion);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,10 @@ public async Task<InstanceDiscoveryMetadataEntry> GetMetadataAsync(Uri authority
{
if (isMtlsEnabled)
{
requestContext.Logger.Info("[Region discovery] Region discovery failed during mTLS Pop. ");

throw new MsalServiceException(
MsalError.RegionRequiredForMtlsPop,
MsalErrorMessage.RegionRequiredForMtlsPopMessage);
// Region is not available — use the global mTLS endpoint
string globalMtlsEnv = GetGlobalMtlsEnvironment(authority, requestContext);
requestContext.Logger.Info($"[Region discovery] Region not available. Using global mTLS environment: {globalMtlsEnv}");
return CreateEntry(authority.Host, globalMtlsEnv);
}

requestContext.Logger.Info("[Region discovery] Not using a regional authority. ");
Expand All @@ -99,6 +98,29 @@ private static InstanceDiscoveryMetadataEntry CreateEntry(string originalEnv, st
};
}

private static string GetGlobalMtlsEnvironment(Uri authority, RequestContext requestContext)
{
string host = authority.Host;

if (KnownMetadataProvider.IsPublicEnvironment(host))
{
return PublicEnvForRegionalMtlsAuth;
}

if (KnownMetadataProvider.TryGetKnownEnviromentPreferredNetwork(host, out var preferredNetworkEnv))
{
host = preferredNetworkEnv;
}

// Replace "login" with "mtlsauth" for mTLS scenarios
if (host.StartsWith("login.", StringComparison.OrdinalIgnoreCase))
{
host = "mtlsauth" + host.Substring("login".Length);
}

return host;
}

private static string GetRegionalizedEnvironment(Uri authority, string region, RequestContext requestContext)
{
string host = authority.Host;
Expand Down Expand Up @@ -127,7 +149,7 @@ private static string GetRegionalizedEnvironment(Uri authority, string region, R
if (requestContext.IsMtlsRequested)
{
// Modify the host to replace "login" with "mtlsauth" for mTLS scenarios
if (host.StartsWith("login"))
if (host.StartsWith("login.", StringComparison.OrdinalIgnoreCase))
{
host = "mtlsauth" + host.Substring("login".Length);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Microsoft.Identity.Test.Integration.HeadlessTests
// POP tests only work on the allow listed SNI app
// and tenant ("bea21ebe-8b64-4d06-9f6d-6a889b120a7c") - MSI team tenant
[TestClass]
public class ClientCredentialsMtlsPopTests
public class ClientCredentialsMtlsPopTests
{
private const string MsiAllowListedAppIdforSNI = "163ffef9-a313-45b4-ab2f-c7e2f5e0e23e";
private const string TokenExchangeUrl = "api://AzureADTokenExchange/.default";
Expand All @@ -46,7 +46,7 @@ public async Task Sni_Gets_Pop_Token_Successfully_TestAsync()
IConfidentialClientApplication confidentialApp = ConfidentialClientApplicationBuilder.Create(MsiAllowListedAppIdforSNI)
.WithAuthority("https://login.microsoftonline.com/bea21ebe-8b64-4d06-9f6d-6a889b120a7c")
.WithAzureRegion("westus3") //test slice region
.WithCertificate(cert, true)
.WithCertificate(cert, true)
.WithTestLogging()
.Build();

Expand Down Expand Up @@ -83,6 +83,69 @@ public async Task Sni_Gets_Pop_Token_Successfully_TestAsync()
"BindingCertificate must match the certificate supplied via WithCertificate().");
}

[DoNotRunOnLinux] // POP is not supported on Linux
[TestMethod]
public async Task Sni_Gets_Pop_Token_WithGlobalEndpoint_TestAsync()
{
// Arrange: validate lab setup before executing the test flow.
_ = await LabResponseHelper.GetAppConfigAsync(KeyVaultSecrets.AppS2S).ConfigureAwait(false);

X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName);

string[] appScopes = new[] { "https://vault.azure.net/.default" };

// Build Confidential Client Application with SNI certificate — NO region configured
IConfidentialClientApplication confidentialApp = ConfidentialClientApplicationBuilder.Create(MsiAllowListedAppIdforSNI)
.WithAuthority("https://login.microsoftonline.com/bea21ebe-8b64-4d06-9f6d-6a889b120a7c")
.WithCertificate(cert, true)
.WithTestLogging()
.Build();

// Act: Acquire token with MTLS Proof of Possession at Request level (global endpoint)
AuthenticationResult authResult = await confidentialApp
.AcquireTokenForClient(appScopes)
.WithMtlsProofOfPossession()
.ExecuteAsync()
.ConfigureAwait(false);

// Assert: Check that the MTLS PoP token acquisition was successful
Assert.IsNotNull(authResult, "The authentication result should not be null.");
Assert.AreEqual(Constants.MtlsPoPTokenType, authResult.TokenType, "Token type should be MTLS PoP");
Assert.IsNotNull(authResult.AccessToken, "Access token should not be null");

Assert.IsNotNull(authResult.BindingCertificate, "BindingCertificate should be set in SNI flow.");
Assert.AreEqual(cert.Thumbprint,
authResult.BindingCertificate.Thumbprint,
"BindingCertificate must match the certificate supplied via WithCertificate().");

// Verify global mTLS endpoint was used (no region prefix)
Assert.IsTrue(
System.Uri.TryCreate(
authResult.AuthenticationResultMetadata.TokenEndpoint,
System.UriKind.Absolute,
out System.Uri tokenEndpointUri),
"Token endpoint should be a valid absolute URI.");
Assert.AreEqual(
"mtlsauth.microsoft.com",
tokenEndpointUri.Host,
"Should use global mtlsauth endpoint when no region is configured.");

// Simulate cache retrieval to verify MTLS configuration is cached properly
authResult = await confidentialApp
.AcquireTokenForClient(appScopes)
.WithMtlsProofOfPossession()
.ExecuteAsync()
.ConfigureAwait(false);

// Assert: Verify that the token was fetched from cache on the second request
Assert.AreEqual(TokenSource.Cache, authResult.AuthenticationResultMetadata.TokenSource, "Token should be retrieved from cache");

Assert.IsNotNull(authResult.BindingCertificate, "BindingCertificate should be set in SNI flow.");
Assert.AreEqual(cert.Thumbprint,
authResult.BindingCertificate.Thumbprint,
"BindingCertificate must match the certificate supplied via WithCertificate().");
}

[DoNotRunOnLinux]
[TestMethod]
public async Task Sni_AssertionFlow_Uses_JwtPop_And_Succeeds_TestAsync()
Expand Down Expand Up @@ -270,5 +333,72 @@ public async Task Sni_AssertionFlow_Uses_JwtPop_And_Acquires_Bearer_Token_TestAs
// Optional: if you rely on regional mTLS endpoints, check the host
StringAssert.Contains(requestUriSeen ?? "", "mtlsauth.microsoft.com");
}

[DoNotRunOnLinux]
[TestMethod]
public async Task Sni_AssertionFlow_GlobalEndpoint_Uses_JwtPop_And_Succeeds_TestAsync()
{
X509Certificate2 cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName);

// Step 1: obtain a real JWT to reuse as the "assertion" — using regional for first leg
IConfidentialClientApplication firstApp = ConfidentialClientApplicationBuilder.Create(MsiAllowListedAppIdforSNI)
.WithAuthority("https://login.microsoftonline.com/bea21ebe-8b64-4d06-9f6d-6a889b120a7c")
.WithAzureRegion("westus3")
.WithCertificate(cert, true)
.WithTestLogging()
.Build();

AuthenticationResult first = await firstApp
.AcquireTokenForClient(new[] { TokenExchangeUrl })
.WithMtlsProofOfPossession()
.ExecuteAsync()
.ConfigureAwait(false);

string assertionJwt = first.AccessToken;
Assert.IsFalse(string.IsNullOrEmpty(assertionJwt), "First leg did not return an access token to reuse as assertion.");

// Step 2: build the assertion-based app — NO region configured (global endpoint)
bool assertionProviderCalled = false;
string requestUriSeen = null;

IConfidentialClientApplication assertionApp = ConfidentialClientApplicationBuilder.Create(MsiAllowListedAppIdforSNI)
.WithExperimentalFeatures()
.WithAuthority("https://login.microsoftonline.com/bea21ebe-8b64-4d06-9f6d-6a889b120a7c")
.WithClientAssertion((AssertionRequestOptions options, CancellationToken ct) =>
{
assertionProviderCalled = true;

return Task.FromResult(new ClientSignedAssertion
{
Assertion = assertionJwt,
TokenBindingCertificate = cert
});
})
.WithTestLogging()
.Build();

// Step 3: second leg should succeed using global mTLS endpoint
AuthenticationResult second = await assertionApp
.AcquireTokenForClient(new[] { "https://vault.azure.net/.default" })
.WithMtlsProofOfPossession()
.OnBeforeTokenRequest(data =>
{
requestUriSeen = data.RequestUri?.ToString();
return Task.CompletedTask;
})
.ExecuteAsync()
.ConfigureAwait(false);

// Success assertions
Assert.IsNotNull(second, "Second leg returned null AuthenticationResult.");
Assert.IsFalse(string.IsNullOrEmpty(second.AccessToken), "Second leg did not return an access token.");
Assert.IsTrue(assertionProviderCalled, "Client assertion provider should have been invoked.");

// Verify global mTLS endpoint was used
Assert.IsFalse(string.IsNullOrEmpty(requestUriSeen), "Expected token request URI to be captured.");
var requestUri = new System.Uri(requestUriSeen);
Assert.AreEqual("mtlsauth.microsoft.com", requestUri.Host,
"Should use global mtlsauth endpoint when no region is configured.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
Expand Down Expand Up @@ -678,28 +678,31 @@ public async Task ClientAssertion_NotCalledWhenTokenFromCacheAsync()
}

[TestMethod]
public async Task WithMtlsPop_AfterPoPDelegate_NoRegion_ThrowsAsync()
public async Task WithMtlsPop_AfterPoPDelegate_NoRegion_UsesGlobalEndpointAsync()
{
using var http = new MockHttpManager();
{
// Arrange – CCA with PoP delegate (returns JWT + cert) but **no AzureRegion configured**
var cert = CertHelper.GetOrCreateTestCert();
http.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(
tokenType: "mtls_pop");

var cca = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId)
.WithExperimentalFeatures(true)
.WithAuthority(TestConstants.AadAuthorityWithMsftTenantId)
.WithClientAssertion(PopDelegate())
.WithHttpManager(http)
.BuildConcrete();

// Act & Assert – should fail because region is missing
var ex = await AssertException.TaskThrowsAsync<MsalClientException>(async () =>
await cca.AcquireTokenForClient(TestConstants.s_scope)
.WithMtlsProofOfPossession()
.ExecuteAsync()
.ConfigureAwait(false))
// Act – should succeed using global mTLS endpoint
AuthenticationResult result = await cca.AcquireTokenForClient(TestConstants.s_scope)
.WithMtlsProofOfPossession()
.ExecuteAsync()
.ConfigureAwait(false);

Assert.AreEqual(MsalError.MtlsPopWithoutRegion, ex.ErrorCode);
Assert.IsNotNull(result.AccessToken);
Assert.AreEqual(Constants.MtlsPoPAuthHeaderPrefix, result.TokenType);
var tokenEndpointUri = new Uri(result.AuthenticationResultMetadata.TokenEndpoint);
Assert.AreEqual("mtlsauth.microsoft.com", tokenEndpointUri.Host);
}
}

Expand Down Expand Up @@ -783,26 +786,28 @@ public async Task BearerClientAssertion_WithPoPDelegate_CanReturnDifferentPairsA
}

[TestMethod]
public async Task WithMtlsAssertion_NoRegion_ThrowsAsync()
public async Task WithMtlsAssertion_NoRegion_UsesGlobalEndpointAsync()
{
using var http = new MockHttpManager();
{
// Arrange – CCA with PoP delegate (returns JWT + cert) but **no AzureRegion configured**
var cert = CertHelper.GetOrCreateTestCert();
http.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage();

var cca = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId)
.WithExperimentalFeatures(true)
.WithAuthority(TestConstants.AadAuthorityWithMsftTenantId)
.WithClientAssertion(PopDelegate())
.WithHttpManager(http)
.BuildConcrete();

// Act & Assert – should fail because region is missing
var ex = await AssertException.TaskThrowsAsync<MsalClientException>(async () =>
await cca.AcquireTokenForClient(TestConstants.s_scope)
.ExecuteAsync()
.ConfigureAwait(false))
// Act – should succeed using global mTLS endpoint
AuthenticationResult result = await cca.AcquireTokenForClient(TestConstants.s_scope)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.AreEqual(MsalError.MtlsBearerWithoutRegion, ex.ErrorCode);
Assert.IsNotNull(result.AccessToken);
var tokenEndpoint = new Uri(result.AuthenticationResultMetadata.TokenEndpoint, UriKind.Absolute);
Assert.AreEqual("mtlsauth.microsoft.com", tokenEndpoint.Host);
}
}

Expand Down
Loading
Loading