Skip to content

Commit

Permalink
Use HTTP Host header for Kerberos auth SPN calculation (dotnet/corefx…
Browse files Browse the repository at this point in the history
…#38465)

Fixed SocketsHttpHandler so that it will use the request's Host header,
if present, as part of building the Service Principal Name (SPN) when
doing Kerberos authentication. This now matches .NET Framework behavior.

Contributes to dotnet/corefx#34697 and dotnet/corefx#27745

Commit migrated from dotnet/corefx@6cb8546
  • Loading branch information
davidsh committed Jun 12, 2019
1 parent 749b2c8 commit 78c1298
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,32 +76,50 @@ private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMe
needDrain = false;
}

string challengeData = challenge.ChallengeData;
if (NetEventSource.IsEnabled)
{
NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Uri: {authUri.AbsoluteUri.ToString()}");
}

// Need to use FQDN normalized host so that CNAME's are traversed.
// Use DNS to do the forward lookup to an A (host) record.
// But skip DNS lookup on IP literals. Otherwise, we would end up
// doing an unintended reverse DNS lookup.
string spn;
UriHostNameType hnt = authUri.HostNameType;
if (hnt == UriHostNameType.IPv6 || hnt == UriHostNameType.IPv4)
// Calculate SPN (Service Principal Name) using the host name of the request.
// Use the request's 'Host' header if available. Otherwise, use the request uri.
string hostName;
if (request.HasHeaders && request.Headers.Host != null)
{
spn = authUri.IdnHost;
// Use the host name without any normalization.
hostName = request.Headers.Host;
if (NetEventSource.IsEnabled)
{
NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: {hostName}");
}
}
else
{
IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost).ConfigureAwait(false);
spn = result.HostName;
// Need to use FQDN normalized host so that CNAME's are traversed.
// Use DNS to do the forward lookup to an A (host) record.
// But skip DNS lookup on IP literals. Otherwise, we would end up
// doing an unintended reverse DNS lookup.
UriHostNameType hnt = authUri.HostNameType;
if (hnt == UriHostNameType.IPv6 || hnt == UriHostNameType.IPv4)
{
hostName = authUri.IdnHost;
}
else
{
IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost).ConfigureAwait(false);
hostName = result.HostName;
}
}
spn = "HTTP/" + spn;

string spn = "HTTP/" + hostName;
if (NetEventSource.IsEnabled)
{
NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: {authUri.IdnHost}, SPN: {spn}");
NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, SPN: {spn}");
}

ChannelBinding channelBinding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint);
NTAuthentication authContext = new NTAuthentication(isServer:false, challenge.SchemeName, challenge.Credential, spn, ContextFlagsPal.Connection, channelBinding);
string challengeData = challenge.ChallengeData;
try
{
while (true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net.Test.Common;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -518,6 +520,10 @@ public static IEnumerable<object[]> ServerUsesWindowsAuthentication_MemberData()
private static bool IsNtlmInstalled => Capability.IsNtlmInstalled();
private static bool IsWindowsServerAvailable => !string.IsNullOrEmpty(Configuration.Http.WindowsServerHttpHost);
private static bool IsDomainJoinedServerAvailable => !string.IsNullOrEmpty(Configuration.Http.DomainJoinedHttpHost);
private static NetworkCredential DomainCredential = new NetworkCredential(
Configuration.Security.ActiveDirectoryUserName,
Configuration.Security.ActiveDirectoryUserPassword,
Configuration.Security.ActiveDirectoryName);

[ConditionalFact(nameof(IsDomainJoinedServerAvailable))]
public async Task Credentials_DomainJoinedServerUsesKerberos_Success()
Expand All @@ -530,19 +536,39 @@ public async Task Credentials_DomainJoinedServerUsesKerberos_Success()
using (HttpClientHandler handler = CreateHttpClientHandler())
using (HttpClient client = CreateHttpClient(handler))
{
handler.Credentials = new NetworkCredential(
Configuration.Security.ActiveDirectoryUserName,
Configuration.Security.ActiveDirectoryUserPassword,
Configuration.Security.ActiveDirectoryName);
handler.Credentials = DomainCredential;

var request = new HttpRequestMessage();
var server = $"http://{Configuration.Http.DomainJoinedHttpHost}/test/auth/kerberos/showidentity.ashx";
request.RequestUri = new Uri(server);
string server = $"http://{Configuration.Http.DomainJoinedHttpHost}/test/auth/kerberos/showidentity.ashx";
using (HttpResponseMessage response = await client.GetAsync(server))
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
string body = await response.Content.ReadAsStringAsync();
_output.WriteLine(body);
}
}
}

// Force HTTP/1.1 since both CurlHandler and SocketsHttpHandler have problems with
// HTTP/2.0 and Windows authentication (due to HTTP/2.0 -> HTTP/1.1 downgrade handling).
// Issue #35195 (for SocketsHttpHandler).
request.Version = new Version(1,1);
[ConditionalFact(nameof(IsDomainJoinedServerAvailable))]
public async Task Credentials_DomainJoinedServerUsesKerberos_UseIpAddressAndHostHeader_Success()
{
if (IsCurlHandler || IsWinHttpHandler)
{
throw new SkipTestException("Skipping test on platform handlers (CurlHandler, WinHttpHandler)");
}

using (HttpClientHandler handler = CreateHttpClientHandler())
using (HttpClient client = CreateHttpClient(handler))
{
handler.Credentials = DomainCredential;

IPAddress[] addresses = Dns.GetHostAddresses(Configuration.Http.DomainJoinedHttpHost);
IPAddress hostIP = addresses.Where(a => a.AddressFamily == AddressFamily.InterNetwork).Select(a => a).First();

var request = new HttpRequestMessage();
request.RequestUri = new Uri($"http://{hostIP}/test/auth/kerberos/showidentity.ashx");
request.Headers.Host = Configuration.Http.DomainJoinedHttpHost;
_output.WriteLine(request.RequestUri.AbsoluteUri.ToString());
_output.WriteLine($"Host: {request.Headers.Host}");

using (HttpResponseMessage response = await client.SendAsync(request))
{
Expand All @@ -569,15 +595,7 @@ public async Task Credentials_ServerUsesWindowsAuthentication_Success(string ser
Configuration.Security.WindowsServerUserName,
Configuration.Security.WindowsServerUserPassword);

var request = new HttpRequestMessage();
request.RequestUri = new Uri(server);

// Force HTTP/1.1 since both CurlHandler and SocketsHttpHandler have problems with
// HTTP/2.0 and Windows authentication (due to HTTP/2.0 -> HTTP/1.1 downgrade handling).
// Issue #35195 (for SocketsHttpHandler).
request.Version = new Version(1,1);

using (HttpResponseMessage response = await client.SendAsync(request))
using (HttpResponseMessage response = await client.GetAsync(server))
{
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
string body = await response.Content.ReadAsStringAsync();
Expand Down

0 comments on commit 78c1298

Please sign in to comment.