From ffad857c2f7c2d13f037dfcea28a3c4b67a55227 Mon Sep 17 00:00:00 2001 From: Tomas Weinfurt Date: Thu, 16 May 2024 10:59:42 -0700 Subject: [PATCH] add support for Impersonation and Delegation to HttpWebRequest (#102038) * initial drop * update * feedback * feedbcak * comment * spacing --- .../System/Net/Http/HttpHandlerDefaults.cs | 2 ++ .../AuthenticationHelper.NtAuth.cs | 16 +++++++------- .../ConnectionPool/HttpConnectionPool.cs | 4 ++-- .../HttpConnectionSettings.cs | 3 +++ .../src/System/Net/HttpWebRequest.cs | 18 +++++++++++++++- .../src/System/Net/WebRequest.cs | 2 +- .../tests/HttpWebRequestTest.cs | 21 ++++++++++++++++++- 7 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/libraries/Common/src/System/Net/Http/HttpHandlerDefaults.cs b/src/libraries/Common/src/System/Net/Http/HttpHandlerDefaults.cs index a9a9beeced8ef8..9924612c8e13c7 100644 --- a/src/libraries/Common/src/System/Net/Http/HttpHandlerDefaults.cs +++ b/src/libraries/Common/src/System/Net/Http/HttpHandlerDefaults.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Threading; +using System.Security.Principal; namespace System.Net.Http { @@ -23,6 +24,7 @@ internal static partial class HttpHandlerDefaults public const bool DefaultUseProxy = true; public const bool DefaultUseDefaultCredentials = false; public const bool DefaultCheckCertificateRevocationList = false; + public const TokenImpersonationLevel DefaultImpersonationLevel = TokenImpersonationLevel.None; public static readonly TimeSpan DefaultPooledConnectionLifetime = Timeout.InfiniteTimeSpan; public static readonly TimeSpan DefaultPooledConnectionIdleTimeout = TimeSpan.FromMinutes(1); public static readonly TimeSpan DefaultExpect100ContinueTimeout = TimeSpan.FromSeconds(1); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs index ae572bbdcdf291..65c355595462fd 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs @@ -8,6 +8,7 @@ using System.Net.Http.Headers; using System.Net.Security; using System.Security.Authentication.ExtendedProtection; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; @@ -72,7 +73,7 @@ private static bool ProxySupportsConnectionAuth(HttpResponseMessage response) return false; } - private static async Task SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, bool isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) + private static async Task SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, TokenImpersonationLevel impersonationLevel, bool isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) { HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false); if (!isProxyAuth && connection.Kind == HttpConnectionKind.Proxy && !ProxySupportsConnectionAuth(response)) @@ -173,7 +174,8 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe Credential = challenge.Credential, TargetName = spn, RequiredProtectionLevel = requiredProtectionLevel, - Binding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint) + Binding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint), + AllowedImpersonationLevel = impersonationLevel }; using NegotiateAuthentication authContext = new NegotiateAuthentication(authClientOptions); @@ -203,7 +205,7 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe if (!IsAuthenticationChallenge(response, isProxyAuth)) { - // Tail response for Negoatiate on successful authentication. Validate it before we proceed. + // Tail response for Negotiate on successful authentication. Validate it before we proceed. authContext.GetOutgoingBlob(challengeData, out statusCode); if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded) { @@ -230,15 +232,15 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe return response!; } - public static Task SendWithNtProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool async, ICredentials proxyCredentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) + public static Task SendWithNtProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool async, ICredentials proxyCredentials, TokenImpersonationLevel impersonationLevel, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) { - return SendWithNtAuthAsync(request, proxyUri, async, proxyCredentials, isProxyAuth: true, connection, connectionPool, cancellationToken); + return SendWithNtAuthAsync(request, proxyUri, async, proxyCredentials, impersonationLevel, isProxyAuth: true, connection, connectionPool, cancellationToken); } - public static Task SendWithNtConnectionAuthAsync(HttpRequestMessage request, bool async, ICredentials credentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) + public static Task SendWithNtConnectionAuthAsync(HttpRequestMessage request, bool async, ICredentials credentials, TokenImpersonationLevel impersonationLevel, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) { Debug.Assert(request.RequestUri != null); - return SendWithNtAuthAsync(request, request.RequestUri, async, credentials, isProxyAuth: false, connection, connectionPool, cancellationToken); + return SendWithNtAuthAsync(request, request.RequestUri, async, credentials, impersonationLevel, isProxyAuth: false, connection, connectionPool, cancellationToken); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs index 0d9f3a2a1b9537..3fc2f43b01a8c7 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs @@ -380,7 +380,7 @@ private Task SendWithNtConnectionAuthAsync(HttpConnection c { if (doRequestAuth && Settings._credentials != null) { - return AuthenticationHelper.SendWithNtConnectionAuthAsync(request, async, Settings._credentials, connection, this, cancellationToken); + return AuthenticationHelper.SendWithNtConnectionAuthAsync(request, async, Settings._credentials, Settings._impersonationLevel, connection, this, cancellationToken); } return SendWithNtProxyAuthAsync(connection, request, async, cancellationToken); @@ -390,7 +390,7 @@ public Task SendWithNtProxyAuthAsync(HttpConnection connect { if (DoProxyAuth && ProxyCredentials is not null) { - return AuthenticationHelper.SendWithNtProxyAuthAsync(request, ProxyUri!, async, ProxyCredentials, connection, this, cancellationToken); + return AuthenticationHelper.SendWithNtProxyAuthAsync(request, ProxyUri!, async, ProxyCredentials, HttpHandlerDefaults.DefaultImpersonationLevel, connection, this, cancellationToken); } return connection.SendAsync(request, async, cancellationToken); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs index a76440e5fb1631..5cd1ee218c3245 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs @@ -8,6 +8,7 @@ using System.Net.Http.Metrics; using System.Net.Security; using System.Runtime.Versioning; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; @@ -29,6 +30,7 @@ internal sealed class HttpConnectionSettings internal bool _preAuthenticate = HttpHandlerDefaults.DefaultPreAuthenticate; internal ICredentials? _credentials; + internal TokenImpersonationLevel _impersonationLevel = HttpHandlerDefaults.DefaultImpersonationLevel; // this is here to support impersonation on HttpWebRequest internal bool _allowAutoRedirect = HttpHandlerDefaults.DefaultAutomaticRedirection; internal int _maxAutomaticRedirections = HttpHandlerDefaults.DefaultMaxAutomaticRedirections; @@ -128,6 +130,7 @@ public HttpConnectionSettings CloneAndNormalize() _defaultCredentialsUsedForProxy = _proxy != null && (_proxy.Credentials == CredentialCache.DefaultCredentials || _defaultProxyCredentials == CredentialCache.DefaultCredentials), _defaultCredentialsUsedForServer = _credentials == CredentialCache.DefaultCredentials, _clientCertificateOptions = _clientCertificateOptions, + _impersonationLevel = _impersonationLevel, }; return settings; diff --git a/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs b/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs index 8f37002cf206b7..7510495533be92 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/HttpWebRequest.cs @@ -13,10 +13,12 @@ using System.Net.Http.Headers; using System.Net.Security; using System.Net.Sockets; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; @@ -122,6 +124,7 @@ private sealed class HttpClientParameters public readonly CookieContainer? CookieContainer; public readonly ServicePoint? ServicePoint; public readonly TimeSpan ContinueTimeout; + public readonly TokenImpersonationLevel ImpersonationLevel; public HttpClientParameters(HttpWebRequest webRequest, bool async) { @@ -144,6 +147,7 @@ public HttpClientParameters(HttpWebRequest webRequest, bool async) CookieContainer = webRequest._cookieContainer; ServicePoint = webRequest._servicePoint; ContinueTimeout = TimeSpan.FromMilliseconds(webRequest.ContinueTimeout); + ImpersonationLevel = webRequest.ImpersonationLevel; } public bool Matches(HttpClientParameters requestParameters) @@ -164,7 +168,8 @@ public bool Matches(HttpClientParameters requestParameters) && ReferenceEquals(ServerCertificateValidationCallback, requestParameters.ServerCertificateValidationCallback) && ReferenceEquals(ClientCertificates, requestParameters.ClientCertificates) && ReferenceEquals(CookieContainer, requestParameters.CookieContainer) - && ReferenceEquals(ServicePoint, requestParameters.ServicePoint); + && ReferenceEquals(ServicePoint, requestParameters.ServicePoint) + && ImpersonationLevel == requestParameters.ImpersonationLevel; } public bool AreParametersAcceptableForCaching() @@ -1666,6 +1671,17 @@ private static HttpClient CreateHttpClient(HttpClientParameters parameters, Http handler.Expect100ContinueTimeout = parameters.ContinueTimeout; client.Timeout = parameters.Timeout; + if (request != null && request.ImpersonationLevel != TokenImpersonationLevel.None) + { + // This is legacy feature and we don't have public API at the moment. + // So we want to process it only if explicitly set. + var settings = typeof(SocketsHttpHandler).GetField("_settings", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(handler); + Debug.Assert(settings != null); + FieldInfo? fi = Type.GetType("System.Net.Http.HttpConnectionSettings, System.Net.Http")?.GetField("_impersonationLevel", BindingFlags.NonPublic | BindingFlags.Instance); + Debug.Assert(fi != null); + fi.SetValue(settings, request.ImpersonationLevel); + } + if (parameters.CookieContainer != null) { handler.CookieContainer = parameters.CookieContainer; diff --git a/src/libraries/System.Net.Requests/src/System/Net/WebRequest.cs b/src/libraries/System.Net.Requests/src/System/Net/WebRequest.cs index e33ffea0fa140d..5442fdeef0facd 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/WebRequest.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/WebRequest.cs @@ -376,7 +376,7 @@ internal static List PrefixList public AuthenticationLevel AuthenticationLevel { get; set; } = AuthenticationLevel.MutualAuthRequested; - public TokenImpersonationLevel ImpersonationLevel { get; set; } = TokenImpersonationLevel.Delegation; + public TokenImpersonationLevel ImpersonationLevel { get; set; } = TokenImpersonationLevel.None; public virtual string? ConnectionGroupName { diff --git a/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs b/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs index 54ad032277d875..c6d693cdf5004c 100644 --- a/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs +++ b/src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs @@ -8,11 +8,11 @@ using System.Linq; using System.Net.Cache; using System.Net.Http; -using System.Net.Http.Functional.Tests; using System.Net.Sockets; using System.Net.Test.Common; using System.Runtime.Serialization.Formatters.Binary; using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; using System.Text; using System.Text.Json; using System.Threading; @@ -1067,6 +1067,25 @@ public void ReadWriteTimeout_NegativeOrZeroValue_Fail() Assert.Throws(() => { request.ReadWriteTimeout = -10; }); } + [Theory] + [InlineData(TokenImpersonationLevel.Delegation)] + [InlineData(TokenImpersonationLevel.Impersonation)] + public async Task ImpersonationLevel_NonDefault_Ok(TokenImpersonationLevel impersonationLevel) + { + await LoopbackServer.CreateClientAndServerAsync(async uri => + { + HttpWebRequest request = WebRequest.CreateHttp(uri); + request.UseDefaultCredentials = true; + // We really don't test the functionality here. + // We need to trigger the Reflection part to make sure it works + // e.g. verify that it was not trimmed away or broken by refactoring. + request.ImpersonationLevel = impersonationLevel; + + using WebResponse response = await GetResponseAsync(request); + Assert.True(request.HaveResponse); + }, server => server.HandleRequestAsync()); + } + [OuterLoop("Uses timeout")] [Theory] [InlineData(false)]