diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index f50d325498..b1100572e9 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -199,7 +199,15 @@ private HttpClient GetHttpClient(X509Certificate2 x509Certificate2, Func headers) { HttpRequestMessage requestMessage = new HttpRequestMessage { RequestUri = endpoint }; + requestMessage.Headers.Accept.Clear(); + +#if NET5_0_OR_GREATER + // On .NET 5.0 and later, HTTP2 is supported through the SDK and Entra is HTTP2 compatible + // Note that HttpClient.DefaultRequestVersion does not work when using HttpRequestMessage objects + requestMessage.Version = HttpVersion.Version20; // Default to HTTP/2 + requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower; // Allow fallback to HTTP/1.1 +#endif if (headers != null) { foreach (KeyValuePair kvp in headers) diff --git a/tests/Microsoft.Identity.Test.Common/Core/Helpers/HttpSnifferClientFactory.cs b/tests/Microsoft.Identity.Test.Common/Core/Helpers/HttpSnifferClientFactory.cs index 0cd6178df8..1390cfe1d3 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Helpers/HttpSnifferClientFactory.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Helpers/HttpSnifferClientFactory.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Net; using System.Net.Http; using System.Security.Cryptography.X509Certificates; +using System.Runtime.InteropServices; using Microsoft.Identity.Client; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Identity.Test.Common { @@ -28,7 +31,24 @@ public HttpSnifferClientFactory() req.Content.LoadIntoBufferAsync().GetAwaiter().GetResult(); LastHttpContentData = req.Content.ReadAsStringAsync().GetAwaiter().GetResult(); } + + // check the .net runtime + var framework = RuntimeInformation.FrameworkDescription; + + // This will match ".NET 5.0", ".NET 6.0", ".NET 7.0", ".NET 8.0", etc. + if (framework.StartsWith(".NET ", StringComparison.OrdinalIgnoreCase)) + { + // Extract the version number + var versionString = framework.Substring(5).Trim(); // e.g., "6.0.0" + if (Version.TryParse(versionString, out var version) && version.Major >= 5) + { + Assert.AreEqual(new Version(2, 0), req.Version, $"Request version mismatch: {req.Version}. MSAL on NET 5+ expects HTTP/2.0 for all requests."); + // ESTS-R endpoint does not support HTTP/2.0, so we don't assert this + } + } + RequestsAndResponses.Add((req, res)); + Trace.WriteLine($"[MSAL][HTTP Request]: {req}"); Trace.WriteLine($"[MSAL][HTTP Response]: {res}"); }); diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index 66cb40d66f..7d22a3cdb0 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Security; @@ -11,6 +12,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; @@ -533,5 +535,48 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() Assert.AreEqual(Num500Errors, requestsMade); } } + + private class CapturingHandler : HttpMessageHandler + { + public HttpRequestMessage CapturedRequest { get; private set; } + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + CapturedRequest = request; + return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK)); + } + } + +#if NET + [TestMethod] + public async Task SendRequestAsync_SetsHttp2VersionAndPolicy() + { + // Arrange + var handler = new CapturingHandler(); + var httpClient = new HttpClient(handler); + var httpClientFactory = Substitute.For(); + httpClientFactory.GetHttpClient().Returns(httpClient); + + var httpManager = new Client.Http.HttpManager(httpClientFactory, disableInternalRetries: true); + + // Act + await httpManager.SendRequestAsync( + new Uri("https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=https://login.microsoftonline.com/common/oauth2/v2.0/authorize"), + null, + null, + HttpMethod.Get, + Substitute.For(), + doNotThrow: true, + bindingCertificate: null, + validateServerCert: null, + cancellationToken: CancellationToken.None, + retryPolicy: Substitute.For() + ).ConfigureAwait(false); + + // Assert + Assert.IsNotNull(handler.CapturedRequest); + Assert.AreEqual(HttpVersion.Version20, handler.CapturedRequest.Version); + Assert.AreEqual(HttpVersionPolicy.RequestVersionOrLower, handler.CapturedRequest.VersionPolicy); + } +#endif } }