runtime: Basic Authentication via HTTPClientHandler not working for Android

Description

The basic authentication via HTTPClientHandler credentials doesn’t work for Android (.net 7.0). Can you confirm, this is a bug or is something wrong with the coding below? Thanks, Michael

Steps to Reproduce

  1. Create MAU project .net 7.0
  2. add the following coding:
HttpClientHandler lClientHandler = new HttpClientHandler();
CancellationTokenSource lCancel = new CancellationTokenSource();
lCancel.CancelAfter(TimeSpan.FromMilliseconds(5000));
lClientHandler.Credentials = new NetworkCredential("<user>", "<password>");
var lClient = new HttpClient(lClientHandler);
var lResponse = await lClient.GetAsync("https://<web address>" , HttpCompletionOption.ResponseContentRead, lCancel.Token).ConfigureAwait(false);

This returns a 401 (Unauthorized) for android. The same code works just fine for windows. I have tested the problem with multiple android emulators (Android 11 and Android 13) and an android device (Android 13). The web service is accessible via browser on the device (and the login/password works as well).

Link to public reproduction project repository

Version with bug

7.0 (current)

Last version that worked well

Unknown/Other

Affected platforms

Android

Affected platform versions

Android 11/ Android 13, probably others as well

Did you find any workaround?

If I set the Authorization manually it works just fine. (Instead of: lClientHandler.Credentials = new NetworkCredential("<user>", "<password>"); for instance use:

var byteArray = Encoding.ASCII.GetBytes("<user>:<password>");
lClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));

) However this workaround doesn’t really work for me, because the code is part of a library implementation I use and which I cannot change. Furthermore this coding works in a Xamarin Android app (non MAUI) without problem.

Relevant log output

No response

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 21 (18 by maintainers)

Most upvoted comments

AFAIK the default native handler that’s used internally by HttpClientHandler on Android won’t include Credentials by default.

Should it? This makes it problematic for anybody who wants to write portable code that runs same on all platforms. It would be great IMHO if people do not need to understand all the platform differences.

@simonrozsival yeah, it’s a bit of a confusing mess… Perhaps handling 401 would make sense, even at the cost of the extra request. It would be enabled by default, with a way to disable it (a property on AndroidMessageHandler) if need be. I suppose most people would be fine with this kind of a breaking change.

AFAIK the default native handler that’s used internally by HttpClientHandler on Android won’t include Credentials by default. I would recommend using the AndroidMessageHandler directly and configuring its PreAuthenticate and PreAuthenticationData properties. What’s also necessary is that the server sends the WWW-Authenticate: Basic header.

Here’s a short sample:

using System.Net;
using Xamarin.Android.Net;

var url = "https://httpbin.org/basic-auth/username/correct-password";

await SendWith(username: "username", password: "wrong-password");
await SendWith(username: "username", password: "correct-password");

async Task SendWith(string username, string password)
{
    Console.WriteLine($"=======");
    Console.WriteLine($"Test username={username}, password={password}");
    Console.WriteLine($"=======");

    var handler = new AndroidMessageHandler
    {
        Credentials = new NetworkCredential(username, password),
    };
    var client = new HttpClient(handler);

    var response = await client.GetAsync(url) as AndroidHttpResponseMessage;
    await Dump(response);

    if (response.RequestNeedsAuthorization)
    {
        handler.PreAuthenticationData = response.RequestedAuthentication.FirstOrDefault(auth => auth.Scheme == AuthenticationScheme.Basic)
             ?? new Exception("In this sample we require Basic auth");
        handler.PreAuthenticate = true;

        response = await client.GetAsync(url) as AndroidHttpResponseMessage;
        await Dump(response);
    }

    Console.WriteLine();
}

async Task Dump(AndroidHttpResponseMessage response)
{
    Console.WriteLine($"Request: {response.RequestMessage}");

    var body = await response.Content.ReadAsStringAsync();
    Console.WriteLine($"Response: {response}");
    Console.WriteLine($"Response body: '{body}'");
}

The expected output is:

[DOTNET] =======
[DOTNET] Test username=username, password=wrong-password
[DOTNET] =======
[DOTNET] Request: Method: GET, RequestUri: 'https://httpbin.org/basic-auth/username/correct-password', Version: 1.1, Content: <null>, Headers:
[DOTNET] {
[DOTNET] }
[DOTNET] Response: StatusCode: 401, ReasonPhrase: 'UNAUTHORIZED', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
[DOTNET] {
[DOTNET]   Access-Control-Allow-Credentials: true
[DOTNET]   Access-Control-Allow-Origin: *
[DOTNET]   Connection: keep-alive
[DOTNET]   Date: Fri, 10 Feb 2023 12:17:26 GMT
[DOTNET]   Server: gunicorn/19.9.0
[DOTNET]   WWW-Authenticate: Basic realm="Fake Realm"
[DOTNET]   X-Android-Received-Millis: 1676031445941
[DOTNET]   X-Android-Response-Source: NETWORK 401
[DOTNET]   X-Android-Selected-Protocol: http/1.1
[DOTNET]   X-Android-Sent-Millis: 1676031445603
[DOTNET]   Content-Length: 0
[DOTNET] }
[DOTNET] Response body: ''
[DOTNET] Request: Method: GET, RequestUri: 'https://httpbin.org/basic-auth/username/correct-password', Version: 1.1, Content: <null>, Headers:
[DOTNET] {
[DOTNET] }
[DOTNET] Response: StatusCode: 401, ReasonPhrase: 'UNAUTHORIZED', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
[DOTNET] {
[DOTNET]   Access-Control-Allow-Credentials: true
[DOTNET]   Access-Control-Allow-Origin: *
[DOTNET]   Connection: keep-alive
[DOTNET]   Date: Fri, 10 Feb 2023 12:17:26 GMT
[DOTNET]   Server: gunicorn/19.9.0
[DOTNET]   WWW-Authenticate: Basic realm="Fake Realm"
[DOTNET]   X-Android-Received-Millis: 1676031446474
[DOTNET]   X-Android-Response-Source: NETWORK 401
[DOTNET]   X-Android-Selected-Protocol: http/1.1
[DOTNET]   X-Android-Sent-Millis: 1676031445992
[DOTNET]   Content-Length: 0
[DOTNET] }
[DOTNET] Response body: ''
[DOTNET] 
[DOTNET] =======
[DOTNET] Test username=username, password=correct-password
[DOTNET] =======
[DOTNET] Request: Method: GET, RequestUri: 'https://httpbin.org/basic-auth/username/correct-password', Version: 1.1, Content: <null>, Headers:
[DOTNET] {
[DOTNET] }
[DOTNET] Response: StatusCode: 401, ReasonPhrase: 'UNAUTHORIZED', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
[DOTNET] {
[DOTNET]   Access-Control-Allow-Credentials: true
[DOTNET]   Access-Control-Allow-Origin: *
[DOTNET]   Connection: keep-alive
[DOTNET]   Date: Fri, 10 Feb 2023 12:17:27 GMT
[DOTNET]   Server: gunicorn/19.9.0
[DOTNET]   WWW-Authenticate: Basic realm="Fake Realm"
[DOTNET]   X-Android-Received-Millis: 1676031446650
[DOTNET]   X-Android-Response-Source: NETWORK 401
[DOTNET]   X-Android-Selected-Protocol: http/1.1
[DOTNET]   X-Android-Sent-Millis: 1676031446491
[DOTNET]   Content-Length: 0
[DOTNET] }
[DOTNET] Response body: ''
[DOTNET] Request: Method: GET, RequestUri: 'https://httpbin.org/basic-auth/username/correct-password', Version: 1.1, Content: <null>, Headers:
[DOTNET] {
[DOTNET] }
[DOTNET] Response: StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
[DOTNET] {
[DOTNET]   Access-Control-Allow-Credentials: true
[DOTNET]   Access-Control-Allow-Origin: *
[DOTNET]   Connection: keep-alive
[DOTNET]   Date: Fri, 10 Feb 2023 12:17:28 GMT
[DOTNET]   Server: gunicorn/19.9.0
[DOTNET]   X-Android-Received-Millis: 1676031447873
[DOTNET]   X-Android-Response-Source: NETWORK 200
[DOTNET]   X-Android-Selected-Protocol: http/1.1
[DOTNET]   X-Android-Sent-Millis: 1676031446664
[DOTNET]   Content-Length: 51
[DOTNET]   Content-Type: application/json
[DOTNET] }
[DOTNET] Response body: '{
[DOTNET]   "authenticated": true, 
[DOTNET]   "user": "username"
[DOTNET] }
[DOTNET] '
[DOTNET]