Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Use HTTP Host header for Kerberos auth SPN calculation #38465

Merged
merged 1 commit into from
Jun 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not FQDN, Kerberos libraries would fall back to default domain? (or fail)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily. In general a short name could be part of an SPN definition. On Windows we frequently see multiple SPNs registered such as "HTTP/myhost" and "HTTP/myhost.contoso.com".

Linux Kerberos is more complicated because it uses the KRB5.CONF file. And at the OS Linux layer there is additional transformation including another FQDN (and possible reverse IP) pass.

Right now, this PR is trying to get us closer to .NET Framework behavior at least on the managed layer. The additional issues I referenced above are about making this even better with more granular control down through the OS layer. That is future work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for explanation.

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}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just have one NetEventSource.Info call that logs uri, hostname, and spn, rather than three separate ones?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had separate calls because I was trying to capture the logic of how the SPN was assembled (i.e. host header present or not, DNS resolution, etc.). But you raise a good point that we now have too many separate log entries for this.

I'll submit a follow-up PR to try to address this.

}

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