From 2aa18a5d5194b3afd00eaf4e2fa0bf98b76f4131 Mon Sep 17 00:00:00 2001 From: Bogdan Gavril Date: Mon, 2 Jun 2025 13:41:08 +0100 Subject: [PATCH 1/4] Use HTTP 2 on .NET --- .../Http/HttpManager.cs | 10 ++++- .../Core/Helpers/HttpSnifferClientFactory.cs | 20 +++++++++ .../CoreTests/HttpTests/HttpManagerTests.cs | 45 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index f50d325498..11a6a1ff36 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 by default 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) @@ -222,7 +230,7 @@ private async Task ExecuteAsync( CancellationToken cancellationToken = default) { using (HttpRequestMessage requestMessage = CreateRequestMessage(endpoint, headers)) - { + { requestMessage.Method = method; requestMessage.Content = body; 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..d0a2748e2f 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; @@ -533,5 +534,49 @@ 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 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); + Assert.AreEqual("abc", handler.CapturedRequest.Headers.GetValues("X-Test").Single()); + } +#endif } } From 5e592eadb6467ef0c2a3d5c7992c21dd41a8e36c Mon Sep 17 00:00:00 2001 From: Bogdan Gavril Date: Mon, 2 Jun 2025 13:42:55 +0100 Subject: [PATCH 2/4] 1 --- .../CoreTests/HttpTests/HttpManagerTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index d0a2748e2f..3896f66be5 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -575,7 +575,6 @@ await httpManager.SendRequestAsync( Assert.IsNotNull(handler.CapturedRequest); Assert.AreEqual(HttpVersion.Version20, handler.CapturedRequest.Version); Assert.AreEqual(HttpVersionPolicy.RequestVersionOrLower, handler.CapturedRequest.VersionPolicy); - Assert.AreEqual("abc", handler.CapturedRequest.Headers.GetValues("X-Test").Single()); } #endif } From 75a6a294d85b4ef388b4c4cb4465dd83e6c626fc Mon Sep 17 00:00:00 2001 From: Bogdan Gavril Date: Mon, 2 Jun 2025 16:39:41 +0100 Subject: [PATCH 3/4] Apply suggestions from code review --- src/client/Microsoft.Identity.Client/Http/HttpManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index 11a6a1ff36..b1100572e9 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -203,7 +203,7 @@ private static HttpRequestMessage CreateRequestMessage(Uri endpoint, IDictionary requestMessage.Headers.Accept.Clear(); #if NET5_0_OR_GREATER - // On .NET 5.0 and later, HTTP2 is supported by default and Entra is HTTP2 compatible + // 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 @@ -230,7 +230,7 @@ private async Task ExecuteAsync( CancellationToken cancellationToken = default) { using (HttpRequestMessage requestMessage = CreateRequestMessage(endpoint, headers)) - { + { requestMessage.Method = method; requestMessage.Content = body; From 1024fe9ced277a537b95faac300878c575a11200 Mon Sep 17 00:00:00 2001 From: Bogdan Gavril Date: Tue, 10 Jun 2025 19:11:41 +0100 Subject: [PATCH 4/4] Test fix --- .../CoreTests/HttpTests/HttpManagerTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index 3896f66be5..7d22a3cdb0 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -12,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; @@ -555,7 +556,7 @@ public async Task SendRequestAsync_SetsHttp2VersionAndPolicy() var httpClientFactory = Substitute.For(); httpClientFactory.GetHttpClient().Returns(httpClient); - var httpManager = new HttpManager(httpClientFactory, disableInternalRetries: true); + var httpManager = new Client.Http.HttpManager(httpClientFactory, disableInternalRetries: true); // Act await httpManager.SendRequestAsync(