Skip to content

Commit

Permalink
Fix SPN used for Negotiate authentication (dotnet#33426)
Browse files Browse the repository at this point in the history
SocketsHttpHandler was not normalizing the DNS name prior to using it for the SPN
(Service Principal Name). So, when using URI's that involve a CNAME, it was using
the CNAME directly and not evaluating it to the normalized FQDN A record of the host.

This change fixes the behavior to match .NET Framework so that CNAMEs are resolved
properly. We can use the standard Dns.GetHostEntryAsync() API to resolve the name.

From a performance perspective, this additional DNS API call is limited to just
the SPN calculation for NT Auth. Calling this API doesn't impact the performance on the
wire since the OS will cache DNS calls.  Wireshark confirms that no additional DNS
protocol packets will be sent.

.NET Framework actually caches the normalized DNS resolution on the ServicePoint object
when it opens up a connections. Thus, it doesn't have to call Dns.GetHostEntryAsync()
for the SPN calculation. While a future PR could further optimize SocketsHttpHandler to
also cache this DNS host name, it isn't clear it would result in measurable performance gain.

I tested this change in a separate Enterprise testing environment I set up. I created
a CNAME for a Windows IIS server in a Windows domain-joined environment and demonstrated that
the Negotiate protocol results in a Kerberos authentication (and doesn't fall back to NTLM).

Fixes #32328
  • Loading branch information
davidsh authored and jlennox committed Dec 16, 2018
1 parent 2f7b222 commit edd48bf
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,9 @@
<Compile Include="System\Net\Http\HttpClientHandler.Core.cs" />
<Compile Include="uap\System\Net\HttpClientHandler.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetGroup)' == 'netcoreapp'">
<Reference Include="System.Net.NameResolution" />
</ItemGroup>
<ItemGroup Condition="'$(TargetGroup)' == 'netcoreapp' OR '$(TargetGroup)' == 'netcoreappaot' OR '$(TargetGroup)' == 'uap'">
<Reference Include="Microsoft.Win32.Primitives" />
<Reference Include="System.Buffers" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -77,7 +78,28 @@ private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMe

string challengeData = challenge.ChallengeData;

string spn = "HTTP/" + authUri.IdnHost;
// 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)
{
spn = authUri.IdnHost;
}
else
{
IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost).ConfigureAwait(false);
spn = result.HostName;
}
spn = "HTTP/" + spn;

if (NetEventSource.IsEnabled)
{
NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: {authUri.IdnHost}, SPN: {spn}");
}

ChannelBinding channelBinding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint);
NTAuthentication authContext = new NTAuthentication(isServer:false, challenge.SchemeName, challenge.Credential, spn, ContextFlagsPal.Connection, channelBinding);
try
Expand Down

0 comments on commit edd48bf

Please sign in to comment.