Clarify default credentials usage for HttpClientHandler on UAP #21672
Conversation
This PR clarifies how credential handling and specifically default logged-on credentials actually works on the UAP platform. First, "default credentials" are the current credentials of the logged-on user, typically on an enterprise network joined to an Active Directory domain. When these credentials are sent to a proxy or server, only Negotiate, Kerberos, or NTLM schemes are used. Those credentials can never be sent via other schemes like basic or digest. .NET Framework traditionally has two ways for developers to specify using default logged-on credentials to send to the server. Either set the .UseDefaultCredentials property to true or specify CredentialCache.DefaultCredentials to the .Credentials property. For proxy credentials, the developer could pass DefaultCredentials via the custom IProxy object or via the .DefaultProxyCredentials property. .NET Framework would send default credentials anywhere (intranet or internet) if specified by the developer. The UAP platform implementation of System.Net.Http uses a different set of rules for sending default credentials. It is based on whether the app has Enterprise Authentication capability as well as if the endpoint is specified in an intranet zone. This design is part of the base native WinInet and WinHTTP stacks. It was a design choice to provide optimum security as well as convenience for developers writing apps for enterprise scenarios. This PR does a few things. First, it keeps the .NET ICredentials properties stored separately so that they correctly round-trip on get and set. .NET NetworkCredential has 3 parameters (domain, user, password). But WinRT PasswordCredential only has 2 parameters (user, password). We now convert to the WinRT objects only when we need to use them. Second, this PR clarifies that the .UseDefaultCredentials property is basically a no-op because the API models of default credentials handling is different on the UAP platform as described above. These platform differences will be documented here: https://github.com/dotnet/corefx/wiki/ApiCompat Fixes #19642 Fixes #21510
cc: @karelz |
{ | ||
// The WinRT PasswordCredential object does not have a special credentials value for "default credentials". | ||
// In general, the UWP HTTP platform automatically manages sending default credentials, if no explicit | ||
// credential was specificed, based on if the app has EnterpriseAuthentication capability and if the endpoint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
specificed [](start = 30, length = 10)
typo: specificed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will fix that typo in the next PR this week since I'm editing the same file.
Is there a doc bug tracking the changes? |
There is no doc bug because I plan to update that document right after I merge the PR. That is what I did for the last few PRs. The document is a wiki and can be edited by anyone on GitHub. |
@mmitche @stephentoub I noticed that the Windows runs have been running 5+ hours now. Is there something wrong with CI? |
{ | ||
// We don't support changing the proxy settings in the NETNative version of HttpClient since it's layered on | ||
// WinRT HttpClient. But we do support passing in explicit proxy credentials, if specified, which we can | ||
// get from the specified or default proxy. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this remove the " But we do support passing in explicit proxy credentials, if specified, which we get from the specified or default proxy" functionality?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand your comment. Are you saying I should put back the comment or revise it in some way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I forgot to remove this from the review: I did see that the code handling explicit proxy credentials has been moved (not removed as I've originally thought).
@@ -167,99 +168,48 @@ public bool PreAuthenticate | |||
|
|||
public bool UseDefaultCredentials | |||
{ | |||
get { return Credentials == null; } | |||
get { return false; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that if the app has Enterprise Auth it will use default credentials and credentials haven't been set. Can we somehow implement capability detection and return the correct state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enterprise Auth is not the only thing that determines if default credentials are used. It also depends on the endpoint itself and whether or not it is listed in the intranet zones.
} | ||
else if (_rtFilter.ServerCredential == null) | ||
{ | ||
// The only way to disable default credentials is to provide credentials. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't this comment true? If yes, it would allow apps to disable DefaultCredentials.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment was not true which is why it is being removed.
// empty values for username and password) indicates that there is no explicit credential. And that means | ||
// that the default logged-on credentials might be sent to the endpoint. | ||
// | ||
// There is currently no WinRT API to turn off sending default credentials other than the capability |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See other comment: isn't setting user/pass credentials going to disable default credentials?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in general setting a non-null credential will always prevent default credentials from being sent since there is an explicit credential. However, having a "null" credential assigned to the properties doesn't mean "no credential". It simply means "no explicit credential" and the end result can be either no credential is sent, or a default logged-on credentials is sent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see: so basically we cannot implement DefaultCredentials=false because it needs to send at least some fake, non-null user-name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so basically we cannot implement DefaultCredentials=false because it needs to send at least some fake, non-null user-name.
Correct. And although I thought about doing that (creating a "fake" username, i.e. guid-string), it would actually break app-compat behavior with prior UWP .NET Core (1.0, 1.1, etc.). It is better to keep the behavior and document the platform differences.
// with empty strings for username and password inside the object. However, one can't assign | ||
// empty strings to those properties; otherwise, it will throw an error. | ||
RTPasswordCredential rtCreds = new RTPasswordCredential(); | ||
if (!string.IsNullOrEmpty(networkCred.UserName)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of calling multiple COM pinvokes why not just decide first if the user/pwd are empty and then call the right RTPasswordCredential ctor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look into doing that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See comment above.
NetworkCredential networkCred = (NetworkCredential)creds; | ||
|
||
// Creating a new WinRT PasswordCredential object with the default constructor ends up | ||
// with empty strings for username and password inside the object. However, one can't assign |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to docs this shouldn't be true: only the user must be not null: https://docs.microsoft.com/en-us/uwp/api/windows.security.credentials.passwordcredential
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I actually re-tested this. The documentation is wrong. That is why I coded it that way in the first place.
Using a null for either the username or password parameter in the constructor or setting the property gives:
"System.ArgumentNullException: Value cannot be null."
Using an empty string for either the username or password parameter in the constructor or setting the property gives:
"System.ArgumentException: The parameter is incorrect."
So, that is why I create the RTPasswordCredential with default constructor first. And then assign the properties only if non-null and non-empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should open a bug or correct the docs then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opened internal bug: 12569816.
|
||
private void SetFilterProxyCredential() | ||
{ | ||
// We don't support changing the proxy settings in the NETNative version of HttpClient since it's layered on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: it's the UAP version as well. NetNative shouldn't have any differences from UAP.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point. I can revise that comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may revise that comment in the next PR depending on if I can get this PR merged since CI is acting weird.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There may be one potential perf improvement when initializing the PasswordCredential.
LGTM
@dotnet-bot Test Windows x64 Debug Build |
@dotnet/dnceng PTAL. This might be related to the scale set issues from yesterday? |
Scale sets have been updated and redeployed. Please retry failed/blocked build. |
Clarify default credentials usage for HttpClientHandler on UAP Commit migrated from dotnet/corefx@d3f9f21
This PR clarifies how credential handling and specifically default
logged-on credentials actually works on the UAP platform.
First, "default credentials" are the current credentials of the
logged-on user, typically on an enterprise network joined to an
Active Directory domain. When these credentials are sent to a
proxy or server, only Negotiate, Kerberos, or NTLM schemes are
used. Those credentials can never be sent via other schemes
like basic or digest.
.NET Framework traditionally has two ways for developers to specify
using default logged-on credentials to send to the server. Either
set the .UseDefaultCredentials property to true or specify
CredentialCache.DefaultCredentials to the .Credentials property.
For proxy credentials, the developer could pass DefaultCredentials
via the custom IProxy object or via the .DefaultProxyCredentials
property.
.NET Framework would send default credentials anywhere (intranet
or internet) if specified by the developer.
The UAP platform implementation of System.Net.Http uses a different
set of rules for sending default credentials. It is based on whether
the app has Enterprise Authentication capability as well as if the
endpoint is specified in an intranet zone. This design is part of
the base native WinInet and WinHTTP stacks. It was a design choice
to provide optimum security as well as convenience for developers
writing apps for enterprise scenarios.
This PR does a few things. First, it keeps the .NET ICredentials
properties stored separately so that they correctly round-trip
on get and set. .NET NetworkCredential has 3 parameters (domain,
user, password). But WinRT PasswordCredential only has 2 parameters
(user, password). We now convert to the WinRT objects only when
we need to use them.
Second, this PR clarifies that the .UseDefaultCredentials property
is basically a no-op because the API models of default credentials
handling is different on the UAP platform as described above.
These platform differences will be documented here:
https://github.com/dotnet/corefx/wiki/ApiCompat
Fixes #19642
Fixes #21510