Skip to content

Commit b5e15dc

Browse files
[release/6.0] fix SendAsync from impersonificated context with default credentials (#59155)
* fix SendAsync from inpersonificated context and default credentials * add missing file * remove dead code * feedback from review * name cleanup Co-authored-by: wfurt <[email protected]>
1 parent 6e67f83 commit b5e15dc

File tree

9 files changed

+508
-134
lines changed

9 files changed

+508
-134
lines changed

src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,12 @@ public override async Task<Byte[]> ReadRequestBodyAsync()
866866
return buffer;
867867
}
868868

869+
public void CompleteRequestProcessing()
870+
{
871+
_contentLength = 0;
872+
_bodyRead = false;
873+
}
874+
869875
public override async Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "", bool isFinal = true, int requestId = 0)
870876
{
871877
MemoryStream headerBytes = new MemoryStream();
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
4+
using System;
5+
using System.ComponentModel;
6+
using System.Linq;
7+
using System.Net;
8+
using System.Runtime.InteropServices;
9+
using System.Security.Cryptography;
10+
using System.Security.Principal;
11+
using System.Threading.Tasks;
12+
using Microsoft.Win32.SafeHandles;
13+
14+
namespace System
15+
{
16+
public class WindowsIdentityFixture : IDisposable
17+
{
18+
public WindowsTestAccount TestAccount { get; private set; }
19+
20+
public WindowsIdentityFixture()
21+
{
22+
TestAccount = new WindowsTestAccount("CorFxTstWiIde01kiu");
23+
}
24+
25+
public void Dispose()
26+
{
27+
TestAccount.Dispose();
28+
}
29+
}
30+
31+
public sealed class WindowsTestAccount : IDisposable
32+
{
33+
private readonly string _userName;
34+
private SafeAccessTokenHandle _accountTokenHandle;
35+
public SafeAccessTokenHandle AccountTokenHandle => _accountTokenHandle;
36+
public string AccountName { get; private set; }
37+
38+
public WindowsTestAccount(string userName)
39+
{
40+
_userName = userName;
41+
CreateUser();
42+
}
43+
44+
private void CreateUser()
45+
{
46+
string testAccountPassword;
47+
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
48+
{
49+
byte[] randomBytes = new byte[33];
50+
rng.GetBytes(randomBytes);
51+
52+
// Add special chars to ensure it satisfies password requirements.
53+
testAccountPassword = Convert.ToBase64String(randomBytes) + "_-As@!%*(1)4#2";
54+
55+
USER_INFO_1 userInfo = new USER_INFO_1
56+
{
57+
usri1_name = _userName,
58+
usri1_password = testAccountPassword,
59+
usri1_priv = 1
60+
};
61+
62+
// Create user and remove/create if already exists
63+
uint result = NetUserAdd(null, 1, ref userInfo, out uint param_err);
64+
65+
// error codes https://docs.microsoft.com/en-us/windows/desktop/netmgmt/network-management-error-codes
66+
// 0 == NERR_Success
67+
if (result == 2224) // NERR_UserExists
68+
{
69+
result = NetUserDel(null, userInfo.usri1_name);
70+
if (result != 0)
71+
{
72+
throw new Win32Exception((int)result);
73+
}
74+
result = NetUserAdd(null, 1, ref userInfo, out param_err);
75+
if (result != 0)
76+
{
77+
throw new Win32Exception((int)result);
78+
}
79+
}
80+
81+
const int LOGON32_PROVIDER_DEFAULT = 0;
82+
const int LOGON32_LOGON_INTERACTIVE = 2;
83+
84+
if (!LogonUser(_userName, ".", testAccountPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out _accountTokenHandle))
85+
{
86+
_accountTokenHandle = null;
87+
throw new Exception($"Failed to get SafeAccessTokenHandle for test account {_userName}", new Win32Exception());
88+
}
89+
90+
bool gotRef = false;
91+
try
92+
{
93+
_accountTokenHandle.DangerousAddRef(ref gotRef);
94+
IntPtr logonToken = _accountTokenHandle.DangerousGetHandle();
95+
AccountName = new WindowsIdentity(logonToken).Name;
96+
}
97+
finally
98+
{
99+
if (gotRef)
100+
_accountTokenHandle.DangerousRelease();
101+
}
102+
}
103+
}
104+
105+
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
106+
private static extern bool LogonUser(string userName, string domain, string password, int logonType, int logonProvider, out SafeAccessTokenHandle safeAccessTokenHandle);
107+
108+
[DllImport("netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
109+
internal static extern uint NetUserAdd([MarshalAs(UnmanagedType.LPWStr)]string servername, uint level, ref USER_INFO_1 buf, out uint parm_err);
110+
111+
[DllImport("netapi32.dll")]
112+
internal static extern uint NetUserDel([MarshalAs(UnmanagedType.LPWStr)]string servername, [MarshalAs(UnmanagedType.LPWStr)]string username);
113+
114+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
115+
internal struct USER_INFO_1
116+
{
117+
public string usri1_name;
118+
public string usri1_password;
119+
public uint usri1_password_age;
120+
public uint usri1_priv;
121+
public string usri1_home_dir;
122+
public string usri1_comment;
123+
public uint usri1_flags;
124+
public string usri1_script_path;
125+
}
126+
127+
public void Dispose()
128+
{
129+
_accountTokenHandle?.Dispose();
130+
131+
uint result = NetUserDel(null, _userName);
132+
133+
// 2221= NERR_UserNotFound
134+
if (result != 0 && result != 2221)
135+
{
136+
throw new Win32Exception((int)result);
137+
}
138+
}
139+
}
140+
}
141+

src/libraries/Common/tests/TestUtilities/TestUtilities.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<Compile Include="System\PlatformDetection.cs" />
3434
<Compile Include="System\PlatformDetection.Unix.cs" />
3535
<Compile Include="System\PlatformDetection.Windows.cs" />
36+
<Compile Include="System\WindowsIdentityFixture.cs" />
3637
<!--
3738
Interop.Library is not designed to support runtime checks therefore we are picking the Windows
3839
variant from the Common folder and adding the missing members manually.

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,6 @@ public HttpConnectionSettings()
7575
allowHttp3 && allowHttp2 ? HttpVersion.Version30 :
7676
allowHttp2 ? HttpVersion.Version20 :
7777
HttpVersion.Version11;
78-
_defaultCredentialsUsedForProxy = _proxy != null && (_proxy.Credentials == CredentialCache.DefaultCredentials || _defaultProxyCredentials == CredentialCache.DefaultCredentials);
79-
_defaultCredentialsUsedForServer = _credentials == CredentialCache.DefaultCredentials;
8078
}
8179

8280
/// <summary>Creates a copy of the settings but with some values normalized to suit the implementation.</summary>
@@ -96,8 +94,6 @@ public HttpConnectionSettings CloneAndNormalize()
9694
_connectTimeout = _connectTimeout,
9795
_credentials = _credentials,
9896
_defaultProxyCredentials = _defaultProxyCredentials,
99-
_defaultCredentialsUsedForProxy = _defaultCredentialsUsedForProxy,
100-
_defaultCredentialsUsedForServer = _defaultCredentialsUsedForServer,
10197
_expect100ContinueTimeout = _expect100ContinueTimeout,
10298
_maxAutomaticRedirections = _maxAutomaticRedirections,
10399
_maxConnectionsPerServer = _maxConnectionsPerServer,
@@ -123,6 +119,8 @@ public HttpConnectionSettings CloneAndNormalize()
123119
_plaintextStreamFilter = _plaintextStreamFilter,
124120
_initialHttp2StreamWindowSize = _initialHttp2StreamWindowSize,
125121
_activityHeadersPropagator = _activityHeadersPropagator,
122+
_defaultCredentialsUsedForProxy = _proxy != null && (_proxy.Credentials == CredentialCache.DefaultCredentials || _defaultProxyCredentials == CredentialCache.DefaultCredentials),
123+
_defaultCredentialsUsedForServer = _credentials == CredentialCache.DefaultCredentials,
126124
};
127125

128126
// TODO: Remove if/when QuicImplementationProvider is removed from System.Net.Quic.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
4+
using System.Linq;
5+
using System.Net.Security;
6+
using System.Net.Test.Common;
7+
using System.Security.Principal;
8+
using System.Threading.Tasks;
9+
10+
using Xunit;
11+
using Xunit.Abstractions;
12+
13+
namespace System.Net.Http.Functional.Tests
14+
{
15+
public class ImpersonatedAuthTests: IClassFixture<WindowsIdentityFixture>
16+
{
17+
public static bool CanRunImpersonatedTests = PlatformDetection.IsWindows && PlatformDetection.IsNotWindowsNanoServer;
18+
private readonly WindowsIdentityFixture _fixture;
19+
private readonly ITestOutputHelper _output;
20+
21+
public ImpersonatedAuthTests(WindowsIdentityFixture windowsIdentityFixture, ITestOutputHelper output)
22+
{
23+
_output = output;
24+
_fixture = windowsIdentityFixture;
25+
26+
Assert.False(_fixture.TestAccount.AccountTokenHandle.IsInvalid);
27+
Assert.False(string.IsNullOrEmpty(_fixture.TestAccount.AccountName));
28+
}
29+
30+
[OuterLoop]
31+
[ConditionalTheory(nameof(CanRunImpersonatedTests))]
32+
[InlineData(true)]
33+
[InlineData(false)]
34+
[PlatformSpecific(TestPlatforms.Windows)]
35+
public async Task DefaultHandler_ImpersonificatedUser_Success(bool useNtlm)
36+
{
37+
await LoopbackServer.CreateClientAndServerAsync(
38+
async uri =>
39+
{
40+
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
41+
requestMessage.Version = new Version(1, 1);
42+
43+
var handler = new HttpClientHandler();
44+
handler.UseDefaultCredentials = true;
45+
46+
using (var client = new HttpClient(handler))
47+
{
48+
HttpResponseMessage response = await client.SendAsync(requestMessage);
49+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
50+
Assert.Equal("foo", await response.Content.ReadAsStringAsync());
51+
52+
string initialUser = response.Headers.GetValues(NtAuthTests.UserHeaderName).First();
53+
54+
_output.WriteLine($"Starting test as {WindowsIdentity.GetCurrent().Name}");
55+
56+
// get token and run another request as different user.
57+
WindowsIdentity.RunImpersonated(_fixture.TestAccount.AccountTokenHandle, () =>
58+
{
59+
_output.WriteLine($"Running test as {WindowsIdentity.GetCurrent().Name}");
60+
Assert.Equal(_fixture.TestAccount.AccountName, WindowsIdentity.GetCurrent().Name);
61+
62+
requestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
63+
requestMessage.Version = new Version(1, 1);
64+
65+
HttpResponseMessage response = client.SendAsync(requestMessage).GetAwaiter().GetResult();
66+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
67+
Assert.Equal("foo", response.Content.ReadAsStringAsync().GetAwaiter().GetResult());
68+
69+
string newUser = response.Headers.GetValues(NtAuthTests.UserHeaderName).First();
70+
Assert.Equal(_fixture.TestAccount.AccountName, newUser);
71+
});
72+
}
73+
},
74+
async server =>
75+
{
76+
await server.AcceptConnectionAsync(async connection =>
77+
{
78+
Task t = useNtlm ? NtAuthTests.HandleNtlmAuthenticationRequest(connection, closeConnection: false) : NtAuthTests.HandleNegotiateAuthenticationRequest(connection, closeConnection: false);
79+
await t;
80+
_output.WriteLine("Finished first request");
81+
82+
// Second request should use new connection as it runs as different user.
83+
// We keep first connection open so HttpClient may be tempted top use it.
84+
await server.AcceptConnectionAsync(async connection =>
85+
{
86+
Task t = useNtlm ? NtAuthTests.HandleNtlmAuthenticationRequest(connection, closeConnection: false) : NtAuthTests.HandleNegotiateAuthenticationRequest(connection, closeConnection: false);
87+
await t;
88+
}).ConfigureAwait(false);
89+
}).ConfigureAwait(false);
90+
});
91+
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)