runtime: Android NTLM: Empty Credentials now throws PlatformNotSupportedException

Description

If an empty network credential is used a recent change is now causing the exception System.PlatformNotSupportedException: 'NTLM authentication is not possible with default credentials on this platform.' to be thrown rather than treating the empty credential as null / not set. This is a regression from .NET 7 behavior.

Reproduction Steps

On .NET Android, run the folllowing code:

string url = "https://url.to/my-iwa-secured-server";
var handler = new SocketsHttpHandler();
var cre = handler.Credentials;
handler.Credentials = CredentialCache.DefaultCredentials; // this is empty string username+password
//OR: handler.Credentials = new NetworkCredential();
HttpClient client = new HttpClient(handler);            
var result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, url)); // Throws

Expected behavior

Returns 401 like .NET 7 would.

Actual behavior

[mono-rt] [ERROR] FATAL UNHANDLED EXCEPTION: System.PlatformNotSupportedException: NTLM authentication is not possible with default credentials on this platform.
[mono-rt]    at System.Net.NegotiateAuthenticationPal.ManagedNtlmNegotiateAuthenticationPal..ctor(NegotiateAuthenticationClientOptions clientOptions)
[mono-rt]    at System.Net.NegotiateAuthenticationPal.Create(NegotiateAuthenticationClientOptions clientOptions)
[mono-rt]    at System.Net.NegotiateAuthenticationPal.ManagedSpnegoNegotiateAuthenticationPal.CreateMechanismForPackage(String packageName)
[mono-rt]    at System.Net.NegotiateAuthenticationPal.ManagedSpnegoNegotiateAuthenticationPal.CreateSpNegoNegotiateMessage(ReadOnlySpan`1 incomingBlob, NegotiateAuthenticationStatusCode& statusCode)
[mono-rt]    at System.Net.NegotiateAuthenticationPal.ManagedSpnegoNegotiateAuthenticationPal.GetOutgoingBlob(ReadOnlySpan`1 incomingBlob, NegotiateAuthenticationStatusCode& statusCode)
[mono-rt]    at System.Net.Security.NegotiateAuthentication.GetOutgoingBlob(ReadOnlySpan`1 incomingBlob, NegotiateAuthenticationStatusCode& statusCode)
[mono-rt]    at System.Net.Security.NegotiateAuthentication.GetOutgoingBlob(String incomingBlob, NegotiateAuthenticationStatusCode& statusCode)
[mono-rt]    at System.Net.Http.AuthenticationHelper.SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
[mono-rt]    at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
[mono-rt]    at System.Net.Http.AuthenticationHelper.SendWithAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean preAuthenticate, Boolean isProxyAuth, Boolean doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken)
[mono-rt]    at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
[mono-rt]    at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
[mono-rt]    at IWATest.MainPage.OnCounterClicked(Object sender, EventArgs e) in E:\sources.tmp\IWATest\IWATest\MainPage.xaml.cs:line 25
[mono-rt]    at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__128_0(Object state)
[mono-rt]    at Android.App.SyncContext.<>c__DisplayClass2_0.<Post>b__0() in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.App/SyncContext.cs:line 36
[mono-rt]    at Java.Lang.Thread.RunnableImplementor.Run() in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Java.Lang/Thread.cs:line 36
[mono-rt]    at Java.Lang.IRunnableInvoker.n_Run(IntPtr jnienv, IntPtr native__this) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Java.Lang.IRunnable.cs:line 84
[mono-rt]    at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PP_V(_JniMarshal_PP_V callback, IntPtr jnienv, IntPtr klazz) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:line 22

Regression?

Yes

Known Workarounds

Check whether the credential is empty before setting, but has effect on already released libraries or cross-platform code.

Configuration

.NET 8 Preview 7 (Android)

Other information

/CC @wfurt @filipnavara as discussed

About this issue

  • Original URL
  • State: closed
  • Created 10 months ago
  • Comments: 32 (21 by maintainers)

Most upvoted comments

Got it! I guess I didn’t clean up enough the first time around. I can confirm that the fix works!

@dotMorten will you have time to test it out on your side please? Thanks!

@dotMorten There it is, it’s from a local Debug build since we don’t publish Android artifacts in the pipelines. Should work with both x64 and arm architectures as managed dlls are not arch specific (only OS-specific). System.Net.Security.dll.zip

I am not familiar with the Android publishing process, but for regular desktop bits, the process is

  • Build repro as self-contained
  • Replace relevant binaries in the publish folder

An alternative is to modify your installed .net sdk bits (overwrite dlls in $DOTNET_ROOT/shared/Microsoft.NetCore.App/<version>), with obvious implications.

Sure. Bring it!

I think we should take this one as well for 8.0 @karelz as it can be viewed as 8.0 regression.

Check whether the credential is empty before setting

You cannot check that. The value is a placeholder that’s always empty.


The question is what is the expected behavior when you specify CredentialCache.DefaultCredentials, the server requests NTLM/Negotiate, and the platform doesn’t support it.

.NET 6 on Linux/macOS could throw PlatformNotSupportedException (1). .NET 7 will likely return you 401 Unauthorized and not throw, on Linux/macOS/Android (1 2). .NET 8 is back to throwing PlatformNotSupportedException.

PlatformNotSupportedException explicitly tells you what you did wrong, what is not supported, and you can avoid it as a developer. Returning 401 Unauthorized is slightly more consistent with other error scenarios, but completely hides the underlying issue from the developer and makes it incredibly difficult to debug.

On API level, the .NET 8 change is that NegotiateAuthentication API throws the PlatformNotSupportedException exception from constructor instead of returning Unsupported from first GetOutgoingBlob call. In that case it provides the API consumer with more actionable information. (On second thought, we do communicate unsupported protocols through the Unsupported status code instead of an exception 🤷 )

We can change HttpClient to handle PlatformNotSupportedException and pass through the 401 Unauthorized response.

@karelz @wfurt Thoughts?