Skip to content

Regression in handling of UseDefaultCredentials by HttpClientHandler in .NET Core #84545

Closed
dotnet/dotnet-api-docs
#9372

Description

@kedia990

Description

Depending on the order in which in which the Credentials and UseDefaultCredentials are initialized for HttpClientHandler, there's a difference in behavior between .NET Framework and Core. More specifically, .NET Framework does not care about order of initialization of those properties, whereas in .NET Core, the order might lead to an unexpected outcome.

Reproduction Steps

We're using the following method to initialize an HttpClient that can be used to access a Kerberized web service. In the common case, the calling user's credentials should be used to authenticate to the service, although we provide the option to specify credentials explicitly as well for certain scenarios. This (library) method may get called by applications in either .NET Framework or Core.

The two important lines in the example below are named A and B for ease of explanation subsequently.

public static HttpClient CreateHttpClient(Uri uri, ICredentials credentials = null) =>
    new HttpClient(new HttpClientHandler
    {
        UseDefaultCredentials = credentials == null,    // line A
        Credentials = credentials,                      // line B
        PreAuthenticate = true
    })
    {
        BaseAddress = uri
    };

Expected behavior

In both .NET Framework and Core, the HttpClient should facilitate access to the service (identified by uri) as follows:

  1. If credentials is explicitly provided, it should use that to access the service, and
  2. If credentials is not provided/null, the service should be accessed using the calling user's credentials ("default credentials").

Actual behavior

In .NET Framework, the HttpClient works as desired in either case, largely because the properties in question are backed by independent fields, applied in SetDefaultOptions(). Also, in .NET Core, if credentials is explicitly provided, the HttpClient uses that correctly.

However, if credentials is left null in .NET Core, the current user's credentials are not used. This is due to the fact that in .NET Core, both UseDefaultCredentials and Credentials properties operate on the same _underlyingHandler.Credentials property. So, with the the function as written in the repro and credentials = null, the following happens:

  1. In Line A, the right-hand expression evaluates to true, causing _underlyingHandler.Credentials = CredentialCache.DefaultCredentials within the setter.
  2. Line B then comes along and (un)sets _underlyingHandler.Credentials = null.
  3. At this point, the effect of UseDefaultCredentials from line A is lost, so when making the API call, we get a HTTP 401.

"Luckily", if Line A and B were inverted, things work out correctly in .NET Core regardless of credentials (and .NET Framework continues to not care about the order):

var client = new HttpClient(new HttpClientHandler
{
    Credentials = credentials,                      // line B
    UseDefaultCredentials = credentials == null,    // line A
    ...
  1. If credentials is null:
    1. Line B sets _underlyingHandler.Credentials = null, and
    2. Line A then sets _underlyingHandler.Credentials = CredentialCache.DefaultCredentials.
  2. If credentials is non-null value X:
    1. Line B sets _underlyingHandler.Credentials = X and
    2. In line A, the RHS evaluates to false, and the inner condition isn't met, so the setter effectively no-op's

So in either scenario, we now have the desired behavior, and a function that works across Core and Framework.

Regression?

Yes: as explained, the code sample provided in the repro works as intended in .NET Framework, but the order of properties introduces a regression in .NET Core.

Known Workarounds

Inverting the order in which the properties in question are specified (i.e., ensure that Credentials is initialized before UseDefaultCredentials) is one way of working around the issue in a way that the sample works on .NET Core (and Framework).

Configuration

Beyond Core vs. Framework, I don't think the specifics really matter, but for completeness: Windows 10 22H2 (March CU, 19045.2728), .NET Core 6.0.15, .NET Framework 4.8, 64-bit.

Other information

This issue looks fairly similar to the one flagged for SmtpClient and addressed in #41271, although I couldn't find a corresponding issue/PR for HttpClient. I suspect that (similar to that one,) it should be possible to make the behavior consistent/intuitive here by having backing fields to better track the intent; happy to take a crack at the PR if we can arrive at a consensus for the direction we'd like to take.

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Apr 9, 2023
  2. antonfirsov commented on Apr 12, 2023

    @antonfirsov
    Contributor

    While this might be perceived as a breaking change (or regression) when migrating from .NET Framework, I think we should keep the current behavior, and document the difference as intentional.

    The only way HttpClientHandler can pass the credentials to the underlying handler is to set _underlyingHandler.Credentials, which means there is no clean way to maintain UseDefaultCredentials and Credentials as independent properties. IMO HttpClientHandler.Credentials should always return the credentials that are being passed to the underlying handler, meaning that Credentials changing after a change to UseDefaultCredentials is an expected "side effect" for me.

    TBH I don't see the why do we have 2 separate properties to control the same thing. Having an extra UseDefaultCredentials property seems like a bad design choice copied from legacy HttpWebRequest, SmtpClient etc. API-s.

    @kedia990 note that you could simplify your code as following to make the intention clear:

    public static HttpClient CreateHttpClient(Uri uri, ICredentials credentials = null) =>
        new HttpClient(new HttpClientHandler
        {
            Credentials = credentials ?? CredentialCache.DefaultCredentials,
            // ...

    @dotnet/ncl any objections against turning this into a documentation issue?

  3. added
    needs-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration
    and removed
    untriagedNew issue has not been triaged by the area owner
    on Apr 12, 2023
  4. antonfirsov commented on Apr 13, 2023

    @antonfirsov
    Contributor

    Triage: we could special-case UseDefaultCredentials == true in Credentials's setter to avoid clearing the credentials in that case, but with that we may break other users depending on the .NET Core/6.0+ behavior. Since we have only one report, it seems to be better to keep & document the behavior for now. We can always reevaluate if more customers hit this issue.

  5. added
    documentationDocumentation bug or enhancement, does not impact product or test code
    on Apr 13, 2023
  6. added this to the 8.0.0 milestone on Apr 13, 2023
  7. removed
    needs-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration
    on Apr 13, 2023
  8. kedia990 commented on Apr 16, 2023

    @kedia990
    Author

    Thanks for your responses, @antonfirsov.

    we could special-case UseDefaultCredentials == true in Credentials's setter to avoid clearing the credentials in that case

    Yeah, that was kinda what I had in mind, although if we think that that itself carries the risk of breaking callers that now rely on .NET Core behavior, we could just document this as a difference / migration caveat between .NET Framework -> Core.

    note that you could simplify your code as following to make the intention clear

    I don't know why I assumed that CredentialCache.DefaultCredentials wouldn't be available in .NET Standard 2.0. It clearly is, and that's indeed an improvement code-clarity-wise. Thanks for the tip.

  9. self-assigned this
    on Oct 4, 2023
  10. ghost locked as resolved and limited conversation to collaborators on Nov 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-System.Net.HttpdocumentationDocumentation bug or enhancement, does not impact product or test code

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions