Skip to content

Commit

Permalink
[Mono.Android] Optional NTLMv2 support in AndroidMessageHandler (#6999)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
simonrozsival authored Jul 26, 2022
1 parent d26e69a commit f348163
Show file tree
Hide file tree
Showing 11 changed files with 480 additions and 19 deletions.
6 changes: 6 additions & 0 deletions Documentation/guides/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,12 @@ than `aapt`.

Added in Xamarin.Android 8.1.

## AndroidUseNegotiateAuthentication

A boolean property which enables support for NTLM/Negotiate authentication in `AndroidMessageHandler`. The feature is disabled by default.

Support for this property was added in .NET 7 and has no effect in "legacy" Xamarin.Android.

## AndroidUseSharedRuntime

A boolean property that
Expand Down
4 changes: 4 additions & 0 deletions src/Mono.Android/ILLink/ILLink.Substitutions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
<method signature="System.Void RegisterPackage(System.String,System.Converter`2&lt;System.String,System.Type&gt;)" body="stub" />
<method signature="System.Void RegisterPackages(System.String[],System.Converter`2&lt;System.String,System.Type&gt;[])" body="stub" />
</type>
<type fullname="Xamarin.Android.Net.AndroidMessageHandler">
<method signature="System.Boolean get_NegotiateAuthenticationIsEnabled()" body="stub" feature="Xamarin.Android.Net.UseNegotiateAuthentication" featurevalue="false" value="false" />
<method signature="System.Boolean get_NegotiateAuthenticationIsEnabled()" body="stub" feature="Xamarin.Android.Net.UseNegotiateAuthentication" featurevalue="true" value="true" />
</type>
</assembly>
</linker>
1 change: 1 addition & 0 deletions src/Mono.Android/Mono.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@
<Compile Include="Xamarin.Android.Net\AuthModuleDigest.cs" />
<Compile Include="Xamarin.Android.Net\IAndroidAuthenticationModule.cs" />
<Compile Include="Xamarin.Android.Net\X509TrustManagerWithValidationCallback.cs" />
<Compile Condition=" '$(TargetFramework)' != 'monoandroid10' " Include="Xamarin.Android.Net\NegotiateAuthenticationHelper.cs" />
<Compile Condition=" '$(TargetFramework)' == 'monoandroid10' " Include="Xamarin.Android.Net\OldAndroidSSLSocketFactory.cs" />
</ItemGroup>

Expand Down
74 changes: 55 additions & 19 deletions src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public int MaxAutomaticRedirections
/// </summary>
/// <value>The pre authentication data.</value>
public AuthenticationData? PreAuthenticationData { get; set; }

/// <summary>
/// If the website requires authentication, this property will contain data about each scheme supported
/// by the server after the response. Note that unauthorized request will return a valid response - you
Expand Down Expand Up @@ -234,12 +234,12 @@ public bool RequestNeedsAuthorization {
/// <summary>
/// <para>
/// If the request is to the server protected with a self-signed (or otherwise untrusted) SSL certificate, the request will
/// fail security chain verification unless the application provides either the CA certificate of the entity which issued the
/// fail security chain verification unless the application provides either the CA certificate of the entity which issued the
/// server's certificate or, alternatively, provides the server public key. Whichever the case, the certificate(s) must be stored
/// in this property in order for AndroidMessageHandler to configure the request to accept the server certificate.</para>
/// <para>AndroidMessageHandler uses a custom <see cref="KeyStore"/> and <see cref="TrustManagerFactory"/> to configure the connection.
/// <para>AndroidMessageHandler uses a custom <see cref="KeyStore"/> and <see cref="TrustManagerFactory"/> to configure the connection.
/// If, however, the application requires finer control over the SSL configuration (e.g. it implements its own TrustManager) then
/// it should leave this property empty and instead derive a custom class from AndroidMessageHandler and override, as needed, the
/// it should leave this property empty and instead derive a custom class from AndroidMessageHandler and override, as needed, the
/// <see cref="ConfigureTrustManagerFactory"/>, <see cref="ConfigureKeyManagerFactory"/> and <see cref="ConfigureKeyStore"/> methods
/// instead</para>
/// </summary>
Expand All @@ -264,6 +264,16 @@ public bool RequestNeedsAuthorization {
/// </summary>
public TimeSpan ReadTimeout { get; set; } = TimeSpan.FromHours (24);

#if !MONOANDROID1_0
/// <summary>
/// A feature switch that determines whether the message handler should attempt to authenticate the user
/// using the NTLM/Negotiate authentication method. Enable the feature by adding
/// <c><AndroidUseNegotiateAuthentication>true</AndroidUseNegotiateAuthentication></c> to your project file.
/// </summary>
static bool NegotiateAuthenticationIsEnabled =>
AppContext.TryGetSwitch ("Xamarin.Android.Net.UseNegotiateAuthentication", out bool isEnabled) && isEnabled;
#endif

/// <summary>
/// <para>
/// Specifies the connect timeout
Expand Down Expand Up @@ -331,12 +341,38 @@ string EncodeUrl (Uri url)
/// <returns>Task in which the request is executed</returns>
/// <param name="request">Request provided by <see cref="System.Net.Http.HttpClient"/></param>
/// <param name="cancellationToken">Cancellation token.</param>
protected override async Task <HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
protected override Task <HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
{
#if !MONOANDROID1_0
if (NegotiateAuthenticationIsEnabled) {
return SendWithNegotiateAuthenticationAsync (request, cancellationToken);
}
#endif

return DoSendAsync (request, cancellationToken);
}

#if !MONOANDROID1_0
async Task <HttpResponseMessage?> SendWithNegotiateAuthenticationAsync (HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await DoSendAsync (request, cancellationToken).ConfigureAwait (false);

if (RequestNeedsAuthorization && NegotiateAuthenticationHelper.RequestNeedsNegotiateAuthentication (this, request, out var requestedAuth)) {
var authenticatedResponse = await NegotiateAuthenticationHelper.SendWithAuthAsync (this, request, requestedAuth, cancellationToken).ConfigureAwait (false);
if (authenticatedResponse != null)
return authenticatedResponse;
}

return response;
}
#endif

internal async Task <HttpResponseMessage> DoSendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
{
AssertSelf ();
if (request == null)
throw new ArgumentNullException (nameof (request));

if (!request.RequestUri.IsAbsoluteUri)
throw new ArgumentException ("Must represent an absolute URI", "request");

Expand Down Expand Up @@ -633,7 +669,7 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
return ret;
}

HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent)
HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent)
{
var contentStream = httpConnection.ErrorStream;

Expand Down Expand Up @@ -796,7 +832,7 @@ void CollectAuthInfo (HttpHeaderValueCollection <AuthenticationHeaderValue> head

RequestedAuthentication = authData.AsReadOnly ();
}

AuthenticationScheme GetAuthScheme (string scheme)
{
if (String.Compare ("basic", scheme, StringComparison.OrdinalIgnoreCase) == 0)
Expand Down Expand Up @@ -851,15 +887,15 @@ void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response
/// <summary>
/// Configure the <see cref="HttpURLConnection"/> before the request is sent. This method is meant to be overriden
/// by applications which need to perform some extra configuration steps on the connection. It is called with all
/// the request headers set, pre-authentication performed (if applicable) but before the request body is set
/// the request headers set, pre-authentication performed (if applicable) but before the request body is set
/// (e.g. for POST requests). The default implementation in AndroidMessageHandler does nothing.
/// </summary>
/// <param name="request">Request data</param>
/// <param name="conn">Pre-configured connection instance</param>
protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnection conn)
{
AssertSelf ();

return Task.CompletedTask;
}

Expand Down Expand Up @@ -905,9 +941,9 @@ internal Task SetupRequestInternal (HttpRequestMessage request, HttpURLConnectio
/// <summary>
/// Create and configure an instance of <see cref="TrustManagerFactory"/>. The <paramref name="keyStore"/> parameter is set to the
/// return value of the <see cref="ConfigureKeyStore"/> method, so it might be null if the application overrode the method and provided
/// 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
/// 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
/// method in which case AndroidMessageHandler will create its own instance of the trust manager factory provided that the <see cref="TrustCerts"/>
/// list contains at least one valid certificate. If there are no valid certificates and this method returns <c>null</c>, no custom
/// list contains at least one valid certificate. If there are no valid certificates and this method returns <c>null</c>, no custom
/// trust manager will be created since that would make all the HTTPS requests fail.
/// </summary>
/// <returns>The trust manager factory.</returns>
Expand All @@ -930,7 +966,7 @@ void AppendEncoding (string encoding, ref List <string>? list)
return;
list.Add (encoding);
}

async Task <HttpURLConnection> SetupRequestInternal (HttpRequestMessage request, URLConnection conn)
{
if (conn == null)
Expand All @@ -951,15 +987,15 @@ void AppendEncoding (string encoding, ref List <string>? list)
if (request.Content != null)
AddHeaders (httpConnection, request.Content.Headers);
AddHeaders (httpConnection, request.Headers);

List <string>? accept_encoding = null;

decompress_here = false;
if ((AutomaticDecompression & DecompressionMethods.GZip) != 0) {
AppendEncoding (GZIP_ENCODING, ref accept_encoding);
decompress_here = true;
}

if ((AutomaticDecompression & DecompressionMethods.Deflate) != 0) {
AppendEncoding (DEFLATE_ENCODING, ref accept_encoding);
decompress_here = true;
Expand All @@ -978,7 +1014,7 @@ void AppendEncoding (string encoding, ref List <string>? list)
if (!String.IsNullOrEmpty (cookieHeaderValue))
httpConnection.SetRequestProperty ("Cookie", cookieHeaderValue);
}

HandlePreAuthentication (httpConnection);
await SetupRequest (request, httpConnection).ConfigureAwait (continueOnCapturedContext: false);;
SetupRequestBody (httpConnection, request);
Expand Down Expand Up @@ -1035,7 +1071,7 @@ void SetupSSL (HttpsURLConnection? httpsConnection, HttpRequestMessage requestMe
// there is no point in changing the behavior of the default SSL socket factory
if (!gotCerts && _callbackTrustManagerHelper == null)
return;

tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm);
tmf?.Init (gotCerts ? keyStore : null); // only use the custom key store if the user defined any trusted certs
}
Expand Down Expand Up @@ -1068,7 +1104,7 @@ void SetupSSL (HttpsURLConnection? httpsConnection, HttpRequestMessage requestMe
return keyStore;
}
}

void HandlePreAuthentication (HttpURLConnection httpConnection)
{
var data = PreAuthenticationData;
Expand Down Expand Up @@ -1114,7 +1150,7 @@ void AddHeaders (HttpURLConnection conn, HttpHeaders headers)
conn.SetRequestProperty (header.Key, header.Value != null ? String.Join (GetHeaderSeparator (header.Key), header.Value) : String.Empty);
}
}

void SetupRequestBody (HttpURLConnection httpConnection, HttpRequestMessage request)
{
if (request.Content == null) {
Expand Down
Loading

0 comments on commit f348163

Please sign in to comment.