Skip to content

Commit f348163

Browse files
[Mono.Android] Optional NTLMv2 support in AndroidMessageHandler (#6999)
Context: dotnet/runtime#62264 Context? https://github.com/wfurt/Ntlm Update `Xamarin.Android.Net.AndroidMessageHandler` to *optionally* support NTLMv2 authentication in .NET 7+. This authentication method is recommended only for legacy services that do not provide any more secure options. If an endpoint requires NTLMv2 authentication and NTMLv2 is not enabled, then the endpoint will return HTTP-401 errors. NTLMv2 authentication can be enabled by setting the `$(AndroidUseNegotiateAuthentication)` MSBuild property to True. If this property is False or isn't set, then NTLMv2 support is linked away during the package build. Example `.csproj` changes to enable NTLMv2 support: <PropertyGroup> <AndroidUseNegotiateAuthentication>true</AndroidUseNegotiateAuthentication> </PropertyGroup> Example C# `HttpClient` usage using NTLMv2 authentication: var cache = new CredentialCache (); cache.Add (serverUri, "Negotiate", new NetworkCredential(username, password, domain)); var handler = new AndroidMessageHandler { Credentials = cache, }; var client = new HttpClient (handler); var response = await client.GetAsync (requestUri); // 200 OK; 401 is NTLMv2 isn't enabled
1 parent d26e69a commit f348163

File tree

11 files changed

+480
-19
lines changed

11 files changed

+480
-19
lines changed

Documentation/guides/building-apps/build-properties.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,12 @@ than `aapt`.
13281328

13291329
Added in Xamarin.Android 8.1.
13301330

1331+
## AndroidUseNegotiateAuthentication
1332+
1333+
A boolean property which enables support for NTLM/Negotiate authentication in `AndroidMessageHandler`. The feature is disabled by default.
1334+
1335+
Support for this property was added in .NET 7 and has no effect in "legacy" Xamarin.Android.
1336+
13311337
## AndroidUseSharedRuntime
13321338

13331339
A boolean property that

src/Mono.Android/ILLink/ILLink.Substitutions.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@
88
<method signature="System.Void RegisterPackage(System.String,System.Converter`2&lt;System.String,System.Type&gt;)" body="stub" />
99
<method signature="System.Void RegisterPackages(System.String[],System.Converter`2&lt;System.String,System.Type&gt;[])" body="stub" />
1010
</type>
11+
<type fullname="Xamarin.Android.Net.AndroidMessageHandler">
12+
<method signature="System.Boolean get_NegotiateAuthenticationIsEnabled()" body="stub" feature="Xamarin.Android.Net.UseNegotiateAuthentication" featurevalue="false" value="false" />
13+
<method signature="System.Boolean get_NegotiateAuthenticationIsEnabled()" body="stub" feature="Xamarin.Android.Net.UseNegotiateAuthentication" featurevalue="true" value="true" />
14+
</type>
1115
</assembly>
1216
</linker>

src/Mono.Android/Mono.Android.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@
370370
<Compile Include="Xamarin.Android.Net\AuthModuleDigest.cs" />
371371
<Compile Include="Xamarin.Android.Net\IAndroidAuthenticationModule.cs" />
372372
<Compile Include="Xamarin.Android.Net\X509TrustManagerWithValidationCallback.cs" />
373+
<Compile Condition=" '$(TargetFramework)' != 'monoandroid10' " Include="Xamarin.Android.Net\NegotiateAuthenticationHelper.cs" />
373374
<Compile Condition=" '$(TargetFramework)' == 'monoandroid10' " Include="Xamarin.Android.Net\OldAndroidSSLSocketFactory.cs" />
374375
</ItemGroup>
375376

src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public int MaxAutomaticRedirections
202202
/// </summary>
203203
/// <value>The pre authentication data.</value>
204204
public AuthenticationData? PreAuthenticationData { get; set; }
205-
205+
206206
/// <summary>
207207
/// If the website requires authentication, this property will contain data about each scheme supported
208208
/// by the server after the response. Note that unauthorized request will return a valid response - you
@@ -234,12 +234,12 @@ public bool RequestNeedsAuthorization {
234234
/// <summary>
235235
/// <para>
236236
/// If the request is to the server protected with a self-signed (or otherwise untrusted) SSL certificate, the request will
237-
/// fail security chain verification unless the application provides either the CA certificate of the entity which issued the
237+
/// fail security chain verification unless the application provides either the CA certificate of the entity which issued the
238238
/// server's certificate or, alternatively, provides the server public key. Whichever the case, the certificate(s) must be stored
239239
/// in this property in order for AndroidMessageHandler to configure the request to accept the server certificate.</para>
240-
/// <para>AndroidMessageHandler uses a custom <see cref="KeyStore"/> and <see cref="TrustManagerFactory"/> to configure the connection.
240+
/// <para>AndroidMessageHandler uses a custom <see cref="KeyStore"/> and <see cref="TrustManagerFactory"/> to configure the connection.
241241
/// If, however, the application requires finer control over the SSL configuration (e.g. it implements its own TrustManager) then
242-
/// it should leave this property empty and instead derive a custom class from AndroidMessageHandler and override, as needed, the
242+
/// it should leave this property empty and instead derive a custom class from AndroidMessageHandler and override, as needed, the
243243
/// <see cref="ConfigureTrustManagerFactory"/>, <see cref="ConfigureKeyManagerFactory"/> and <see cref="ConfigureKeyStore"/> methods
244244
/// instead</para>
245245
/// </summary>
@@ -264,6 +264,16 @@ public bool RequestNeedsAuthorization {
264264
/// </summary>
265265
public TimeSpan ReadTimeout { get; set; } = TimeSpan.FromHours (24);
266266

267+
#if !MONOANDROID1_0
268+
/// <summary>
269+
/// A feature switch that determines whether the message handler should attempt to authenticate the user
270+
/// using the NTLM/Negotiate authentication method. Enable the feature by adding
271+
/// <c><AndroidUseNegotiateAuthentication>true</AndroidUseNegotiateAuthentication></c> to your project file.
272+
/// </summary>
273+
static bool NegotiateAuthenticationIsEnabled =>
274+
AppContext.TryGetSwitch ("Xamarin.Android.Net.UseNegotiateAuthentication", out bool isEnabled) && isEnabled;
275+
#endif
276+
267277
/// <summary>
268278
/// <para>
269279
/// Specifies the connect timeout
@@ -331,12 +341,38 @@ string EncodeUrl (Uri url)
331341
/// <returns>Task in which the request is executed</returns>
332342
/// <param name="request">Request provided by <see cref="System.Net.Http.HttpClient"/></param>
333343
/// <param name="cancellationToken">Cancellation token.</param>
334-
protected override async Task <HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
344+
protected override Task <HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
345+
{
346+
#if !MONOANDROID1_0
347+
if (NegotiateAuthenticationIsEnabled) {
348+
return SendWithNegotiateAuthenticationAsync (request, cancellationToken);
349+
}
350+
#endif
351+
352+
return DoSendAsync (request, cancellationToken);
353+
}
354+
355+
#if !MONOANDROID1_0
356+
async Task <HttpResponseMessage?> SendWithNegotiateAuthenticationAsync (HttpRequestMessage request, CancellationToken cancellationToken)
357+
{
358+
var response = await DoSendAsync (request, cancellationToken).ConfigureAwait (false);
359+
360+
if (RequestNeedsAuthorization && NegotiateAuthenticationHelper.RequestNeedsNegotiateAuthentication (this, request, out var requestedAuth)) {
361+
var authenticatedResponse = await NegotiateAuthenticationHelper.SendWithAuthAsync (this, request, requestedAuth, cancellationToken).ConfigureAwait (false);
362+
if (authenticatedResponse != null)
363+
return authenticatedResponse;
364+
}
365+
366+
return response;
367+
}
368+
#endif
369+
370+
internal async Task <HttpResponseMessage> DoSendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
335371
{
336372
AssertSelf ();
337373
if (request == null)
338374
throw new ArgumentNullException (nameof (request));
339-
375+
340376
if (!request.RequestUri.IsAbsoluteUri)
341377
throw new ArgumentException ("Must represent an absolute URI", "request");
342378

@@ -633,7 +669,7 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
633669
return ret;
634670
}
635671

636-
HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent)
672+
HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent)
637673
{
638674
var contentStream = httpConnection.ErrorStream;
639675

@@ -796,7 +832,7 @@ void CollectAuthInfo (HttpHeaderValueCollection <AuthenticationHeaderValue> head
796832

797833
RequestedAuthentication = authData.AsReadOnly ();
798834
}
799-
835+
800836
AuthenticationScheme GetAuthScheme (string scheme)
801837
{
802838
if (String.Compare ("basic", scheme, StringComparison.OrdinalIgnoreCase) == 0)
@@ -851,15 +887,15 @@ void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response
851887
/// <summary>
852888
/// Configure the <see cref="HttpURLConnection"/> before the request is sent. This method is meant to be overriden
853889
/// by applications which need to perform some extra configuration steps on the connection. It is called with all
854-
/// the request headers set, pre-authentication performed (if applicable) but before the request body is set
890+
/// the request headers set, pre-authentication performed (if applicable) but before the request body is set
855891
/// (e.g. for POST requests). The default implementation in AndroidMessageHandler does nothing.
856892
/// </summary>
857893
/// <param name="request">Request data</param>
858894
/// <param name="conn">Pre-configured connection instance</param>
859895
protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnection conn)
860896
{
861897
AssertSelf ();
862-
898+
863899
return Task.CompletedTask;
864900
}
865901

@@ -905,9 +941,9 @@ internal Task SetupRequestInternal (HttpRequestMessage request, HttpURLConnectio
905941
/// <summary>
906942
/// Create and configure an instance of <see cref="TrustManagerFactory"/>. The <paramref name="keyStore"/> parameter is set to the
907943
/// return value of the <see cref="ConfigureKeyStore"/> method, so it might be null if the application overrode the method and provided
908-
/// no key store. It will not be <c>null</c> when the default implementation is used. The application can return <c>null</c> from this
944+
/// no key store. It will not be <c>null</c> when the default implementation is used. The application can return <c>null</c> from this
909945
/// method in which case AndroidMessageHandler will create its own instance of the trust manager factory provided that the <see cref="TrustCerts"/>
910-
/// list contains at least one valid certificate. If there are no valid certificates and this method returns <c>null</c>, no custom
946+
/// list contains at least one valid certificate. If there are no valid certificates and this method returns <c>null</c>, no custom
911947
/// trust manager will be created since that would make all the HTTPS requests fail.
912948
/// </summary>
913949
/// <returns>The trust manager factory.</returns>
@@ -930,7 +966,7 @@ void AppendEncoding (string encoding, ref List <string>? list)
930966
return;
931967
list.Add (encoding);
932968
}
933-
969+
934970
async Task <HttpURLConnection> SetupRequestInternal (HttpRequestMessage request, URLConnection conn)
935971
{
936972
if (conn == null)
@@ -951,15 +987,15 @@ void AppendEncoding (string encoding, ref List <string>? list)
951987
if (request.Content != null)
952988
AddHeaders (httpConnection, request.Content.Headers);
953989
AddHeaders (httpConnection, request.Headers);
954-
990+
955991
List <string>? accept_encoding = null;
956992

957993
decompress_here = false;
958994
if ((AutomaticDecompression & DecompressionMethods.GZip) != 0) {
959995
AppendEncoding (GZIP_ENCODING, ref accept_encoding);
960996
decompress_here = true;
961997
}
962-
998+
963999
if ((AutomaticDecompression & DecompressionMethods.Deflate) != 0) {
9641000
AppendEncoding (DEFLATE_ENCODING, ref accept_encoding);
9651001
decompress_here = true;
@@ -978,7 +1014,7 @@ void AppendEncoding (string encoding, ref List <string>? list)
9781014
if (!String.IsNullOrEmpty (cookieHeaderValue))
9791015
httpConnection.SetRequestProperty ("Cookie", cookieHeaderValue);
9801016
}
981-
1017+
9821018
HandlePreAuthentication (httpConnection);
9831019
await SetupRequest (request, httpConnection).ConfigureAwait (continueOnCapturedContext: false);;
9841020
SetupRequestBody (httpConnection, request);
@@ -1035,7 +1071,7 @@ void SetupSSL (HttpsURLConnection? httpsConnection, HttpRequestMessage requestMe
10351071
// there is no point in changing the behavior of the default SSL socket factory
10361072
if (!gotCerts && _callbackTrustManagerHelper == null)
10371073
return;
1038-
1074+
10391075
tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm);
10401076
tmf?.Init (gotCerts ? keyStore : null); // only use the custom key store if the user defined any trusted certs
10411077
}
@@ -1068,7 +1104,7 @@ void SetupSSL (HttpsURLConnection? httpsConnection, HttpRequestMessage requestMe
10681104
return keyStore;
10691105
}
10701106
}
1071-
1107+
10721108
void HandlePreAuthentication (HttpURLConnection httpConnection)
10731109
{
10741110
var data = PreAuthenticationData;
@@ -1114,7 +1150,7 @@ void AddHeaders (HttpURLConnection conn, HttpHeaders headers)
11141150
conn.SetRequestProperty (header.Key, header.Value != null ? String.Join (GetHeaderSeparator (header.Key), header.Value) : String.Empty);
11151151
}
11161152
}
1117-
1153+
11181154
void SetupRequestBody (HttpURLConnection httpConnection, HttpRequestMessage request)
11191155
{
11201156
if (request.Content == null) {

0 commit comments

Comments
 (0)