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)
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
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.
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 you401 Unauthorized
and not throw, on Linux/macOS/Android (1 2). .NET 8 is back to throwingPlatformNotSupportedException
.PlatformNotSupportedException
explicitly tells you what you did wrong, what is not supported, and you can avoid it as a developer. Returning401 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 thePlatformNotSupportedException
exception from constructor instead of returningUnsupported
from firstGetOutgoingBlob
call. In that case it provides the API consumer with more actionable information. (On second thought, we do communicate unsupported protocols through theUnsupported
status code instead of an exception 🤷 )We can change HttpClient to handle
PlatformNotSupportedException
and pass through the401 Unauthorized
response.@karelz @wfurt Thoughts?