Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit be79afd

Browse files
authored
[release/2.1] Fix windows auth when we receive a 401 after auth completes (#32243)
PR #31589 introduced a regression in the case where we receive another 401 after Windows auth completes (this is caused by the user being successfully authenticated, but not authorized to access the specified resource). We were incorrectly draining the response in this case. As a result, the user would receive a disposed response and eventually throw an ObjectDisposedException. The fix is to adjust the logic here where we only drain if we know we are going to send another request for the rest of the Windows auth (Negotiate/NTLM) challenge-response handshake. Add new unit test for Windows auth scenario.
1 parent 199e52d commit be79afd

File tree

3 files changed

+142
-5
lines changed

3 files changed

+142
-5
lines changed

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMe
5656
challenge.AuthenticationType == AuthenticationType.Ntlm)
5757
{
5858
bool isNewConnection = false;
59+
bool needDrain = true;
5960
try
6061
{
6162
if (response.Headers.ConnectionClose.GetValueOrDefault())
@@ -70,10 +71,7 @@ private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMe
7071
connectionPool.IncrementConnectionCount();
7172
connection.Acquire();
7273
isNewConnection = true;
73-
}
74-
else
75-
{
76-
await connection.DrainResponseAsync(response).ConfigureAwait(false);
74+
needDrain = false;
7775
}
7876

7977
string challengeData = challenge.ChallengeData;
@@ -92,6 +90,11 @@ private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMe
9290
break;
9391
}
9492

93+
if (needDrain)
94+
{
95+
await connection.DrainResponseAsync(response).ConfigureAwait(false);
96+
}
97+
9598
SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(challenge.SchemeName, challengeResponse), isProxyAuth);
9699

97100
response = await InnerSendAsync(request, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false);
@@ -100,7 +103,7 @@ private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMe
100103
break;
101104
}
102105

103-
await connection.DrainResponseAsync(response).ConfigureAwait(false);
106+
needDrain = true;
104107
}
105108
}
106109
finally
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Threading.Tasks;
6+
7+
using Xunit;
8+
using Xunit.Abstractions;
9+
10+
namespace System.Net.Http.Functional.Tests
11+
{
12+
public class NtAuthServer : IDisposable
13+
{
14+
// When tests projects are run in parallel, overlapping port ranges can cause a race condition
15+
// when looking for free ports during dynamic port allocation.
16+
private const int PortMin = 5001;
17+
private const int PortMax = 8000;
18+
19+
private HttpListener _listener;
20+
private Task _serverTask;
21+
22+
public NtAuthServer(AuthenticationSchemes authSchemes)
23+
{
24+
// Create server.
25+
CreateServer(authSchemes);
26+
27+
// Start listening for requests.
28+
_serverTask = Task.Run(async () =>
29+
{
30+
try
31+
{
32+
while (true)
33+
{
34+
HttpListenerContext context = await _listener.GetContextAsync();
35+
36+
bool noAccess = (context.Request.RawUrl == NoAccessUrl);
37+
if (noAccess)
38+
{
39+
context.Response.AddHeader("Www-Authenticate", "Negotiate");
40+
}
41+
42+
context.Response.StatusCode = noAccess ? 401 : 200;
43+
context.Response.ContentLength64 = 0;
44+
context.Response.OutputStream.Close();
45+
}
46+
}
47+
catch (HttpListenerException)
48+
{
49+
// Ignore.
50+
}
51+
});
52+
}
53+
54+
public void Dispose()
55+
{
56+
_listener.Stop();
57+
_serverTask.Wait();
58+
}
59+
60+
public string BaseUrl { get; private set; }
61+
public string NoAccessUrl => "/noaccess";
62+
63+
private void CreateServer(AuthenticationSchemes authSchemes)
64+
{
65+
for (int port = PortMin; port < PortMax; port++)
66+
{
67+
string url = $"http://localhost:{port}/";
68+
69+
_listener = new HttpListener();
70+
_listener.Prefixes.Add(url);
71+
_listener.AuthenticationSchemes = authSchemes;
72+
73+
try
74+
{
75+
_listener.Start();
76+
BaseUrl = url;
77+
return;
78+
}
79+
catch (HttpListenerException)
80+
{
81+
}
82+
}
83+
84+
throw new Exception("Failed to locate a free port.");
85+
}
86+
}
87+
88+
public class NtAuthServers : IDisposable
89+
{
90+
public readonly NtAuthServer NtlmServer = new NtAuthServer(AuthenticationSchemes.Ntlm);
91+
public readonly NtAuthServer NegotiateServer = new NtAuthServer(AuthenticationSchemes.Negotiate);
92+
93+
public void Dispose()
94+
{
95+
NtlmServer.Dispose();
96+
NegotiateServer.Dispose();
97+
}
98+
}
99+
100+
public class NtAuthTests : IClassFixture<NtAuthServers>
101+
{
102+
private readonly NtAuthServers _servers;
103+
private readonly ITestOutputHelper _output;
104+
105+
public NtAuthTests(NtAuthServers servers, ITestOutputHelper output)
106+
{
107+
_servers = servers;
108+
_output = output;
109+
}
110+
111+
[OuterLoop]
112+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows), nameof(PlatformDetection.IsNotWindowsNanoServer))] // HttpListener doesn't support nt auth on non-Windows platforms
113+
[InlineData(true, HttpStatusCode.OK)]
114+
[InlineData(true, HttpStatusCode.Unauthorized)]
115+
[InlineData(false, HttpStatusCode.OK)]
116+
[InlineData(false, HttpStatusCode.Unauthorized)]
117+
public async Task GetAsync_NtAuthServer_ExpectedStatusCode(bool ntlm, HttpStatusCode expectedStatusCode)
118+
{
119+
NtAuthServer server = ntlm ? _servers.NtlmServer : _servers.NegotiateServer;
120+
121+
var handler = new HttpClientHandler();
122+
handler.UseDefaultCredentials = true;
123+
using (var client = new HttpClient(handler))
124+
{
125+
client.BaseAddress = new Uri(server.BaseUrl);
126+
string path = expectedStatusCode == HttpStatusCode.Unauthorized ? server.NoAccessUrl : "";
127+
HttpResponseMessage response = await client.GetAsync(path);
128+
129+
Assert.Equal(expectedStatusCode, response.StatusCode);
130+
}
131+
}
132+
}
133+
}

src/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
<Compile Include="HttpClientHandlerTest.Decompression.cs" />
133133
<Compile Include="HttpClientTest.netcoreapp.cs" />
134134
<Compile Include="HttpMethodTest.netcoreapp.cs" />
135+
<Compile Include="NtAuthTests.cs" />
135136
<Compile Include="ReadOnlyMemoryContentTest.cs" />
136137
<Compile Include="SocketsHttpHandlerTest.cs" />
137138
</ItemGroup>

0 commit comments

Comments
 (0)