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

Fix SPN used for Negotiate authentication #33426

Merged
merged 2 commits into from
Nov 12, 2018
Merged

Fix SPN used for Negotiate authentication #33426

merged 2 commits into from
Nov 12, 2018

Conversation

davidsh
Copy link
Contributor

@davidsh davidsh commented Nov 12, 2018

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

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
@davidsh davidsh added this to the 3.0 milestone Nov 12, 2018
@davidsh davidsh self-assigned this Nov 12, 2018
@davidsh davidsh requested review from stephentoub, geoffkizer and a team November 12, 2018 18:54
@davidsh davidsh added the tenet-compatibility Incompatibility with previous versions or .NET Framework label Nov 12, 2018
string spn;
if (authUri.HostNameType == UriHostNameType.IPv6 || authUri.HostNameType == UriHostNameType.IPv4)
{
spn = "HTTP/" + authUri.IdnHost;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to place brackets around the IPv6 address to work around #28863?

Copy link
Contributor Author

@davidsh davidsh Nov 12, 2018

Choose a reason for hiding this comment

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

Do we need to place brackets around the IPv6 address to work around #28863?

I don't think we have tested this scenario. Both .NET Framework and .NET Core currently have this as-is. And my PR doesn't change this.

Practically speaking, NT Auth is unlikely to work in a scenario where IP addresses are used in the URI. IP address based SPNs are not registered by default in Kerberos (Windows Active Directory). So, they would have to be custom registered by an Enterprise. And typically that would not include any brackets. I've never seen Enterprises even use IP addresses for SPNs.

The safe thing to do for this PR is to leave the behavior as-is, and not add brackets right now. But we could open up an issue for further investigation if we want.

Copy link
Contributor

Choose a reason for hiding this comment

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

In that case this seems like a reasonable behavior.

@@ -77,7 +78,23 @@ 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.
Copy link
Member

Choose a reason for hiding this comment

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

Should we move the comment rather into the else-branch?

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 think the comment is in the right place since it describes the logic to calculate the SPN. But I will revise the comment because it needs to describe that we skip DNS lookup in cases of IP literals.

@@ -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>
Copy link
Member

Choose a reason for hiding this comment

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

We didn't already have this because we were relying on the underlying implementation (e.g. System.Net.Sockets) to do the original Dns call?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. I needed to add it in order to compile.

string spn = "HTTP/" + authUri.IdnHost;
// Need to use FQDN normalized host so that CNAME's are traversed.
string spn;
if (authUri.HostNameType == UriHostNameType.IPv6 || authUri.HostNameType == UriHostNameType.IPv4)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: can you store HostNameType into a local so that in the common case we only need to access it once rather than twice? It looks like it caches but even then that it's not super-cheap or inlineable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will fix.

}
else
{
IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost);
Copy link
Member

Choose a reason for hiding this comment

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

.ConfigureAwait(false)

Copy link
Member

Choose a reason for hiding this comment

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

Is it possible this will throw? If it does, is the correct answer to let that exception propagate, or is there a fallback we should use (e.g. do the same thing that's done in the if block)? I don't know the answer, so I'm asking :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be rare for this to fail because it already succeeded prior to this in order to establish the TCP connection in the first place.

It would be better to let any exception propagate. This is similar to other places in this code that would throw exceptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.ConfigureAwait(false)

Fixed.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, thanks.

{
IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost);
spn = "HTTP/" + result.HostName;
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: maybe instead of doing:

string spn;
if (...)
{
    spn = "HTTP/" + authUri.IdnHost;
}
else
{
    ...
    spn = "HTTP/" + result.HostName;
}

do:

string spn;
if (...)
{
    spn = authUri.IdnHost;
}
else
{
    ...
    spn = result.HostName;
}
spn = "HTTP/" + spn;

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is this better? Is seems slower because we are doing two assignments of spn field.

I don't think it is really less lines of IL or memory because the "HTTP/" string should only occur once.

Copy link
Member

Choose a reason for hiding this comment

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

Why is this better? Is seems slower because we are doing two assignments of spn field.

From my perspective it's better because it's centralizing some logic, namely the concatenation of the "HTTP/" prefix. There's also no additional field assignment, as spn here is a local, not a field. And the IL is a bit longer otherwise, as there are two calls to Concat instead of one:
https://sharplab.io/#v2:EYLgtghgzgLgpgJwDQxASwDYB8ACAGAAhwEYBuAWACh8jiA6AOThgsqpwGYiAmAgYQIBvKgVFEuJQgFliACgCqCNAQCuSgJQixwygEgxBRWgASAe1gMIYOABUAngAc4BABYA7GAQC8qpXTMWVraOcKwGBpIEUA5uYeGiaABmBLLunl4+RgEwltb2TnQAkgAKAG4ALARYWK4e3plK2bnBBSWlAGyalPGiOj3h0W7eBABExjY2xQD0IwQA1L5oRQAmbtlx8QC+WvFwGFBwO+F9/WKDw2MT07MLACJuUHQA4szZAKIeCHayakuFq9l1P5zDkghtwttuj0cAB2KIxcGQgxHTi0aTcBRKRZdAw6fRiLIg5r5ZxpYa/YGBPIhcERYiEQa0sRJFJkjKGRpEoIkoplSrVWrpBomLnU1plTpHXFSnrnHwU/5rEFM0RInp7A4y3pagYxYb3R4vGDvT7fBUAkFAppgnVq+Jy0bjSYzebw2I62FuxFUTZAA=

But it's just a small thing. Whatever you prefer is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. I'll make the change.

And I like the https://sharplab.io example! That is a nice website for this kind of analysis.

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

Thanks.

@davidsh
Copy link
Contributor Author

davidsh commented Nov 12, 2018

@dotnet-bot test Outerloop Windows x64 Debug Build
@dotnet-bot test Outerloop Linux x64 Debug Build
@dotnet-bot test Outerloop NETFX x86 Debug Build
@dotnet-bot test Outerloop UWP CoreCLR x64 Debug Build
@dotnet-bot test Outerloop OSX x64 Debug Build

Copy link
Member

@wfurt wfurt left a comment

Choose a reason for hiding this comment

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

LGTM.

@davidsh
Copy link
Contributor Author

davidsh commented Nov 12, 2018

CI failures unrelated to PR.

@davidsh davidsh merged commit 684114f into dotnet:master Nov 12, 2018
@davidsh davidsh deleted the spn_fix_30 branch November 12, 2018 22:47
jlennox pushed a commit to jlennox/corefx that referenced this pull request Dec 16, 2018
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
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
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 dotnet/corefx#32328 


Commit migrated from dotnet/corefx@684114f
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
tenet-compatibility Incompatibility with previous versions or .NET Framework
Projects
None yet
Development

Successfully merging this pull request may close these issues.

.NET Core 2.1 SocketsHttpHandler does not use Negotiate / SPNego
5 participants