Skip to content

Commit

Permalink
add support for Impersonation and Delegation to HttpWebRequest (#102038)
Browse files Browse the repository at this point in the history
* initial drop

* update

* feedback

* feedbcak

* comment

* spacing
  • Loading branch information
wfurt authored May 16, 2024
1 parent 044e9d3 commit ffad857
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -72,7 +73,7 @@ private static bool ProxySupportsConnectionAuth(HttpResponseMessage response)
return false;
}

private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, bool isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
private static async Task<HttpResponseMessage> 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))
Expand Down Expand Up @@ -173,7 +174,8 @@ private static async Task<HttpResponseMessage> 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);
Expand Down Expand Up @@ -203,7 +205,7 @@ private static async Task<HttpResponseMessage> 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)
{
Expand All @@ -230,15 +232,15 @@ private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMe
return response!;
}

public static Task<HttpResponseMessage> SendWithNtProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool async, ICredentials proxyCredentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
public static Task<HttpResponseMessage> 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<HttpResponseMessage> SendWithNtConnectionAuthAsync(HttpRequestMessage request, bool async, ICredentials credentials, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
public static Task<HttpResponseMessage> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ private Task<HttpResponseMessage> 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);
Expand All @@ -390,7 +390,7 @@ public Task<HttpResponseMessage> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
{
Expand All @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ internal static List<WebRequestPrefixElement> 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
{
Expand Down
21 changes: 20 additions & 1 deletion src/libraries/System.Net.Requests/tests/HttpWebRequestTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1067,6 +1067,25 @@ public void ReadWriteTimeout_NegativeOrZeroValue_Fail()
Assert.Throws<ArgumentOutOfRangeException>(() => { 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)]
Expand Down

0 comments on commit ffad857

Please sign in to comment.