diff --git a/.gitignore b/.gitignore index 4ee73a1121..db8bf68815 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ AppPackages Microsoft.IdentityModel.Clients.ActiveDirectory.XML *.htm .nugetPackageRoot/ +**/.mono/ # Created by https://www.gitignore.io/api/visualstudio diff --git a/src/client/Microsoft.Identity.Client/Instance/Discovery/RegionAndMtlsDiscoveryProvider.cs b/src/client/Microsoft.Identity.Client/Instance/Discovery/RegionAndMtlsDiscoveryProvider.cs index a2ec8962e3..5238347fb4 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Discovery/RegionAndMtlsDiscoveryProvider.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Discovery/RegionAndMtlsDiscoveryProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; @@ -16,6 +17,14 @@ internal class RegionAndMtlsDiscoveryProvider : IRegionDiscoveryProvider public const string PublicEnvForRegional = "login.microsoft.com"; public const string PublicEnvForRegionalMtlsAuth = "mtlsauth.microsoft.com"; + // Map of unsupported sovereign cloud hosts for mTLS PoP to their error messages + private static readonly Dictionary s_unsupportedMtlsHosts = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "login.usgovcloudapi.net", MsalErrorMessage.MtlsPopNotSupportedForUsGovCloudApiMessage }, + { "login.chinacloudapi.cn", MsalErrorMessage.MtlsPopNotSupportedForChinaCloudApiMessage } + }; + public RegionAndMtlsDiscoveryProvider(IHttpManager httpManager) { _regionManager = new RegionManager(httpManager); @@ -23,6 +32,30 @@ public RegionAndMtlsDiscoveryProvider(IHttpManager httpManager) public async Task GetMetadataAsync(Uri authority, RequestContext requestContext) { + // Fail fast: Check for unsupported mTLS hosts before any region discovery + if (requestContext.MtlsCertificate != null) + { + string host = authority.Host; + + // Check if host is in the unsupported list + if (s_unsupportedMtlsHosts.TryGetValue(host, out string errorMessage)) + { + requestContext.Logger.Error($"[Region discovery] mTLS PoP is not supported for host: {host}"); + throw new MsalClientException( + MsalError.MtlsPopNotSupportedForEnvironment, + errorMessage); + } + + // Check if host starts with "login." + if (!host.StartsWith("login.", StringComparison.OrdinalIgnoreCase)) + { + requestContext.Logger.Error($"[Region discovery] mTLS PoP requires hosts to start with 'login.': {host}"); + throw new MsalClientException( + MsalError.MtlsPopNotSupportedForEnvironment, + MsalErrorMessage.MtlsPopNotSupportedForNonLoginHostMessage); + } + } + string region = null; bool isMtlsEnabled = requestContext.MtlsCertificate != null; diff --git a/src/client/Microsoft.Identity.Client/MsalError.cs b/src/client/Microsoft.Identity.Client/MsalError.cs index 5b0a480323..c7775c8d2c 100644 --- a/src/client/Microsoft.Identity.Client/MsalError.cs +++ b/src/client/Microsoft.Identity.Client/MsalError.cs @@ -1207,6 +1207,12 @@ public static class MsalError /// public const string MtlsNotSupportedForManagedIdentity = "mtls_not_supported_for_managed_identity"; + /// + /// What happened? mTLS Proof of Possession (mTLS PoP) is not supported for the specified sovereign cloud environment. + /// Mitigation: Use the supported alternative endpoint for the sovereign cloud environment. + /// + public const string MtlsPopNotSupportedForEnvironment = "mtls_pop_not_supported_for_environment"; + /// /// What happened? The operation attempted to force a token refresh while also using a token hash. /// These two options are incompatible because forcing a refresh bypasses token caching, diff --git a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs index efff53181c..5289509ed6 100644 --- a/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs +++ b/src/client/Microsoft.Identity.Client/MsalErrorMessage.cs @@ -444,6 +444,9 @@ public static string InvalidTokenProviderResponseValue(string invalidValueName) public const string MtlsNotSupportedForManagedIdentityMessage = "IMDSv2 flow is not supported on .NET Framework 4.6.2. Cryptographic operations required for managed identity authentication are unavailable on this platform."; public const string MtlsNotSupportedForNonWindowsMessage = "mTLS PoP with Managed Identity is not supported on this OS. See https://aka.ms/msal-net-pop."; public const string RegionRequiredForMtlsPopMessage = "Regional auto-detect failed. mTLS Proof-of-Possession requires a region to be specified, as there is no global endpoint for mTLS. See https://aka.ms/msal-net-pop for details."; + public const string MtlsPopNotSupportedForUsGovCloudApiMessage = "login.usgovcloudapi.net is not supported for mTLS PoP, please use login.microsoftonline.us"; + public const string MtlsPopNotSupportedForChinaCloudApiMessage = "login.chinacloudapi.cn is not supported for mTLS PoP, please use login.partner.microsoftonline.cn"; + public const string MtlsPopNotSupportedForNonLoginHostMessage = "mTLS PoP is only supported for hosts that start with 'login.'. The provided authority host does not meet this requirement. See https://aka.ms/msal-net-pop for details."; public const string ForceRefreshAndTokenHasNotCompatible = "Cannot specify ForceRefresh and AccessTokenSha256ToRefresh in the same request."; public const string RequestTimeOut = "Request to the endpoint timed out."; public const string MalformedOidcAuthorityFormat = "Possible cause: When using Entra External ID, you didn't append /v2.0, for example {0}/v2.0\""; diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index e0db4ab2d2..0db0290228 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -9,3 +9,4 @@ Microsoft.Identity.Client.ManagedIdentityPopExtensions static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAttributes(string attributeJson) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder +const Microsoft.Identity.Client.MsalError.MtlsPopNotSupportedForEnvironment = "mtls_pop_not_supported_for_environment" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index 27bb35f952..4741e5090d 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -9,3 +9,4 @@ Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(S static Microsoft.Identity.Client.Extensibility.ConfidentialClientApplicationBuilderExtensions.WithCertificate(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder builder, System.Func> certificateProvider, Microsoft.Identity.Client.AppConfig.CertificateOptions certificateOptions) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAttributes(string attributeJson) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MtlsPopNotSupportedForEnvironment = "mtls_pop_not_supported_for_environment" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index 2c4a88e00f..37f859d5dd 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -9,3 +9,4 @@ static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquire Microsoft.Identity.Client.ManagedIdentityPopExtensions static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAttributes(string attributeJson) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MtlsPopNotSupportedForEnvironment = "mtls_pop_not_supported_for_environment" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index 2c4a88e00f..37f859d5dd 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -9,3 +9,4 @@ static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquire Microsoft.Identity.Client.ManagedIdentityPopExtensions static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAttributes(string attributeJson) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MtlsPopNotSupportedForEnvironment = "mtls_pop_not_supported_for_environment" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index 2c4a88e00f..37f859d5dd 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -9,3 +9,4 @@ static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquire Microsoft.Identity.Client.ManagedIdentityPopExtensions static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAttributes(string attributeJson) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MtlsPopNotSupportedForEnvironment = "mtls_pop_not_supported_for_environment" -> string diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index 2c4a88e00f..37f859d5dd 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -9,3 +9,4 @@ static Microsoft.Identity.Client.Extensibility.AbstractConfidentialClientAcquire Microsoft.Identity.Client.ManagedIdentityPopExtensions static Microsoft.Identity.Client.ManagedIdentityPopExtensions.WithMtlsProofOfPossession(this Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder builder) -> Microsoft.Identity.Client.AcquireTokenForManagedIdentityParameterBuilder Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder.WithAttributes(string attributeJson) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder +const Microsoft.Identity.Client.MsalError.MtlsPopNotSupportedForEnvironment = "mtls_pop_not_supported_for_environment" -> string diff --git a/test_results.txt b/test_results.txt new file mode 100644 index 0000000000..be5bb9449a --- /dev/null +++ b/test_results.txt @@ -0,0 +1,23 @@ +Build started 01/29/2026 14:27:45. + 1>Project "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj" on node 1 (Restore target(s)). + 1>_GetAllRestoreProjectPathItems: + Determining projects to restore... + 1>Project "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj" (1) is building "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/src/client/Microsoft.Identity.Client.Desktop/Microsoft.Identity.Client.Desktop.csproj" (2:8) on node 1 (_GenerateProjectRestoreGraph target(s)). + 2:8>Project "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/src/client/Microsoft.Identity.Client.Desktop/Microsoft.Identity.Client.Desktop.csproj" (2:8) is building "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/src/client/Microsoft.Identity.Client.Desktop/Microsoft.Identity.Client.Desktop.csproj" (2:12) on node 1 (_GenerateProjectRestoreGraphPerFramework target(s)). + 2>/usr/share/dotnet/sdk/8.0.417/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(90,5): error NETSDK1100: To build a project targeting Windows on this operating system, set the EnableWindowsTargeting property to true. [/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/src/client/Microsoft.Identity.Client.Desktop/Microsoft.Identity.Client.Desktop.csproj::TargetFramework=netcoreapp3.1] + 2>Done Building Project "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/src/client/Microsoft.Identity.Client.Desktop/Microsoft.Identity.Client.Desktop.csproj" (_GenerateProjectRestoreGraphPerFramework target(s)) -- FAILED. + 2>Done Building Project "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/src/client/Microsoft.Identity.Client.Desktop/Microsoft.Identity.Client.Desktop.csproj" (_GenerateProjectRestoreGraph target(s)) -- FAILED. + 1>Done Building Project "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj" (Restore target(s)) -- FAILED. + +Build FAILED. + + "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/tests/Microsoft.Identity.Test.Unit/Microsoft.Identity.Test.Unit.csproj" (Restore target) (1) -> + "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/src/client/Microsoft.Identity.Client.Desktop/Microsoft.Identity.Client.Desktop.csproj" (_GenerateProjectRestoreGraph target) (2:8) -> + "/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/src/client/Microsoft.Identity.Client.Desktop/Microsoft.Identity.Client.Desktop.csproj" (_GenerateProjectRestoreGraphPerFramework target) (2:12) -> + (ProcessFrameworkReferences target) -> + /usr/share/dotnet/sdk/8.0.417/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(90,5): error NETSDK1100: To build a project targeting Windows on this operating system, set the EnableWindowsTargeting property to true. [/home/runner/work/microsoft-authentication-library-for-dotnet/microsoft-authentication-library-for-dotnet/src/client/Microsoft.Identity.Client.Desktop/Microsoft.Identity.Client.Desktop.csproj::TargetFramework=netcoreapp3.1] + + 0 Warning(s) + 1 Error(s) + +Time Elapsed 00:00:01.61 diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs index a6f21e4f95..e916056bea 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs @@ -669,9 +669,7 @@ public async Task MtlsPop_ValidateExpectedUrlAsync() [DataTestMethod] [DataRow("login.microsoftonline.com", "mtlsauth.microsoft.com")] [DataRow("login.microsoftonline.us", "mtlsauth.microsoftonline.us")] - [DataRow("login.usgovcloudapi.net", "mtlsauth.microsoftonline.us")] [DataRow("login.partner.microsoftonline.cn", "mtlsauth.partner.microsoftonline.cn")] - [DataRow("login.chinacloudapi.cn", "mtlsauth.partner.microsoftonline.cn")] [DataRow("login.sovcloud-identity.fr", "mtlsauth.sovcloud-identity.fr")] [DataRow("login.sovcloud-identity.de", "mtlsauth.sovcloud-identity.de")] [DataRow("login.sovcloud-identity.sg", "mtlsauth.sovcloud-identity.sg")] @@ -732,6 +730,81 @@ public async Task PublicAndSovereignCloud_UsesPreferredNetwork_AndNoDiscovery_As } } + [DataTestMethod] + [DataRow("login.usgovcloudapi.net", MsalErrorMessage.MtlsPopNotSupportedForUsGovCloudApiMessage)] + [DataRow("login.chinacloudapi.cn", MsalErrorMessage.MtlsPopNotSupportedForChinaCloudApiMessage)] + public async Task UnsupportedSovereignHosts_ThrowsMsalClientException_Async(string unsupportedHost, string expectedErrorMessage) + { + // Arrange + string authorityUrl = $"https://{unsupportedHost}/17b189bc-2b81-4ec5-aa51-3e628cbc931b"; + + using (var envContext = new EnvVariableContext()) + { + Environment.SetEnvironmentVariable("REGION_NAME", EastUsRegion); + + using (var harness = new MockHttpAndServiceBundle()) + { + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(authorityUrl) + .WithHttpManager(harness.HttpManager) + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithCertificate(s_testCertificate) + .Build(); + + // Act & Assert + var exception = await Assert.ThrowsExceptionAsync(async () => + { + await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + }).ConfigureAwait(false); + + Assert.AreEqual(MsalError.MtlsPopNotSupportedForEnvironment, exception.ErrorCode); + Assert.AreEqual(expectedErrorMessage, exception.Message); + } + } + } + + [DataTestMethod] + [DataRow("mtlsauth.microsoft.com")] + [DataRow("sts.windows.net")] + [DataRow("graph.microsoft.com")] + public async Task NonLoginHosts_ThrowsMsalClientException_Async(string nonLoginHost) + { + // Arrange + string authorityUrl = $"https://{nonLoginHost}/17b189bc-2b81-4ec5-aa51-3e628cbc931b"; + + using (var envContext = new EnvVariableContext()) + { + Environment.SetEnvironmentVariable("REGION_NAME", EastUsRegion); + + using (var harness = new MockHttpAndServiceBundle()) + { + var app = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(authorityUrl) + .WithHttpManager(harness.HttpManager) + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithCertificate(s_testCertificate) + .Build(); + + // Act & Assert + var exception = await Assert.ThrowsExceptionAsync(async () => + { + await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + }).ConfigureAwait(false); + + Assert.AreEqual(MsalError.MtlsPopNotSupportedForEnvironment, exception.ErrorCode); + Assert.AreEqual(MsalErrorMessage.MtlsPopNotSupportedForNonLoginHostMessage, exception.Message); + } + } + } + [TestMethod] public async Task AcquireTokenForClient_WithMtlsPop_NonStandardCloudAsync() {