diff --git a/Documentation/guides/building-apps/build-properties.md b/Documentation/guides/building-apps/build-properties.md
index 843d9871c23..eb1c02ad601 100644
--- a/Documentation/guides/building-apps/build-properties.md
+++ b/Documentation/guides/building-apps/build-properties.md
@@ -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
diff --git a/src/Mono.Android/ILLink/ILLink.Substitutions.xml b/src/Mono.Android/ILLink/ILLink.Substitutions.xml
index 96dff01b2b6..55ac10132dc 100644
--- a/src/Mono.Android/ILLink/ILLink.Substitutions.xml
+++ b/src/Mono.Android/ILLink/ILLink.Substitutions.xml
@@ -8,5 +8,9 @@
+
+
+
+
diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj
index 8cc95be552f..8c87b408b59 100644
--- a/src/Mono.Android/Mono.Android.csproj
+++ b/src/Mono.Android/Mono.Android.csproj
@@ -368,6 +368,7 @@
+
diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
index fc446353310..439a9083e40 100644
--- a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
+++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
@@ -202,7 +202,7 @@ public int MaxAutomaticRedirections
///
/// The pre authentication data.
public AuthenticationData? PreAuthenticationData { get; set; }
-
+
///
/// 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
@@ -234,12 +234,12 @@ public bool RequestNeedsAuthorization {
///
///
/// 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.
- /// AndroidMessageHandler uses a custom and to configure the connection.
+ /// AndroidMessageHandler uses a custom and 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
/// , and methods
/// instead
///
@@ -264,6 +264,16 @@ public bool RequestNeedsAuthorization {
///
public TimeSpan ReadTimeout { get; set; } = TimeSpan.FromHours (24);
+#if !MONOANDROID1_0
+ ///
+ /// 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
+ /// true to your project file.
+ ///
+ static bool NegotiateAuthenticationIsEnabled =>
+ AppContext.TryGetSwitch ("Xamarin.Android.Net.UseNegotiateAuthentication", out bool isEnabled) && isEnabled;
+#endif
+
///
///
/// Specifies the connect timeout
@@ -331,12 +341,38 @@ string EncodeUrl (Uri url)
/// Task in which the request is executed
/// Request provided by
/// Cancellation token.
- protected override async Task SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
+ protected override Task SendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+#if !MONOANDROID1_0
+ if (NegotiateAuthenticationIsEnabled) {
+ return SendWithNegotiateAuthenticationAsync (request, cancellationToken);
+ }
+#endif
+
+ return DoSendAsync (request, cancellationToken);
+ }
+
+#if !MONOANDROID1_0
+ async Task 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 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");
@@ -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;
@@ -796,7 +832,7 @@ void CollectAuthInfo (HttpHeaderValueCollection head
RequestedAuthentication = authData.AsReadOnly ();
}
-
+
AuthenticationScheme GetAuthScheme (string scheme)
{
if (String.Compare ("basic", scheme, StringComparison.OrdinalIgnoreCase) == 0)
@@ -851,7 +887,7 @@ void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response
///
/// Configure the 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.
///
/// Request data
@@ -859,7 +895,7 @@ void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response
protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnection conn)
{
AssertSelf ();
-
+
return Task.CompletedTask;
}
@@ -905,9 +941,9 @@ internal Task SetupRequestInternal (HttpRequestMessage request, HttpURLConnectio
///
/// Create and configure an instance of . The parameter is set to the
/// return value of the method, so it might be null if the application overrode the method and provided
- /// no key store. It will not be null when the default implementation is used. The application can return null from this
+ /// no key store. It will not be null when the default implementation is used. The application can return null from this
/// method in which case AndroidMessageHandler will create its own instance of the trust manager factory provided that the
- /// list contains at least one valid certificate. If there are no valid certificates and this method returns null, no custom
+ /// list contains at least one valid certificate. If there are no valid certificates and this method returns null, no custom
/// trust manager will be created since that would make all the HTTPS requests fail.
///
/// The trust manager factory.
@@ -930,7 +966,7 @@ void AppendEncoding (string encoding, ref List ? list)
return;
list.Add (encoding);
}
-
+
async Task SetupRequestInternal (HttpRequestMessage request, URLConnection conn)
{
if (conn == null)
@@ -951,7 +987,7 @@ void AppendEncoding (string encoding, ref List ? list)
if (request.Content != null)
AddHeaders (httpConnection, request.Content.Headers);
AddHeaders (httpConnection, request.Headers);
-
+
List ? accept_encoding = null;
decompress_here = false;
@@ -959,7 +995,7 @@ void AppendEncoding (string encoding, ref List ? list)
AppendEncoding (GZIP_ENCODING, ref accept_encoding);
decompress_here = true;
}
-
+
if ((AutomaticDecompression & DecompressionMethods.Deflate) != 0) {
AppendEncoding (DEFLATE_ENCODING, ref accept_encoding);
decompress_here = true;
@@ -978,7 +1014,7 @@ void AppendEncoding (string encoding, ref List ? list)
if (!String.IsNullOrEmpty (cookieHeaderValue))
httpConnection.SetRequestProperty ("Cookie", cookieHeaderValue);
}
-
+
HandlePreAuthentication (httpConnection);
await SetupRequest (request, httpConnection).ConfigureAwait (continueOnCapturedContext: false);;
SetupRequestBody (httpConnection, request);
@@ -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
}
@@ -1068,7 +1104,7 @@ void SetupSSL (HttpsURLConnection? httpsConnection, HttpRequestMessage requestMe
return keyStore;
}
}
-
+
void HandlePreAuthentication (HttpURLConnection httpConnection)
{
var data = PreAuthenticationData;
@@ -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) {
diff --git a/src/Mono.Android/Xamarin.Android.Net/NegotiateAuthenticationHelper.cs b/src/Mono.Android/Xamarin.Android.Net/NegotiateAuthenticationHelper.cs
new file mode 100644
index 00000000000..de025c7b4fd
--- /dev/null
+++ b/src/Mono.Android/Xamarin.Android.Net/NegotiateAuthenticationHelper.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Security;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Xamarin.Android.Net
+{
+ // This code is heavily inspired by System.Net.Http.AuthenticationHelper
+ internal static class NegotiateAuthenticationHelper
+ {
+ const int MaxRequests = 10;
+
+ internal class RequestedNegotiateAuthenticationData
+ {
+ public string AuthType { get; init; }
+ public bool IsProxyAuth { get; init; }
+ public NetworkCredential Credential { get; init; }
+ }
+
+ internal static bool RequestNeedsNegotiateAuthentication (
+ AndroidMessageHandler handler,
+ HttpRequestMessage request,
+ [NotNullWhen (true)] out RequestedNegotiateAuthenticationData? requestedAuth)
+ {
+ requestedAuth = null;
+
+ IEnumerable authenticationData = handler.RequestedAuthentication ?? Array.Empty ();
+ foreach (var auth in authenticationData) {
+ if (TryGetSupportedAuthType (auth.Challenge, out var authType)) {
+ var credentials = auth.UseProxyAuthentication ? handler.Proxy?.Credentials : handler.Credentials;
+ var correspondingCredential = credentials?.GetCredential (request.RequestUri, authType);
+
+ if (correspondingCredential != null) {
+ requestedAuth = new RequestedNegotiateAuthenticationData {
+ IsProxyAuth = auth.UseProxyAuthentication,
+ AuthType = authType,
+ Credential = correspondingCredential
+ };
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ internal static async Task SendWithAuthAsync (
+ AndroidMessageHandler handler,
+ HttpRequestMessage request,
+ RequestedNegotiateAuthenticationData requestedAuth,
+ CancellationToken cancellationToken)
+ {
+ using var authContext = new NegotiateAuthentication (
+ new NegotiateAuthenticationClientOptions {
+ Package = requestedAuth.AuthType,
+ Credential = requestedAuth.Credential,
+ TargetName = await GetTargetName (handler, request, requestedAuth.IsProxyAuth, cancellationToken).ConfigureAwait (false),
+ RequiredProtectionLevel = requestedAuth.IsProxyAuth ? ProtectionLevel.None : ProtectionLevel.Sign,
+ }
+ );
+
+ // we need to make sure that the handler doesn't override the authorization header
+ // with the user defined pre-authentication data
+ var originalPreAuthenticate = handler.PreAuthenticate;
+ handler.PreAuthenticate = false;
+
+ try {
+ return await DoSendWithAuthAsync (handler, request, authContext, requestedAuth, cancellationToken);
+ } finally {
+ handler.PreAuthenticate = originalPreAuthenticate;
+ }
+ }
+
+ static bool TryGetSupportedAuthType (string challenge, out string authType)
+ {
+ var spaceIndex = challenge.IndexOf (' ');
+ authType = spaceIndex == -1 ? challenge : challenge.Substring (0, spaceIndex);
+
+ return authType.Equals ("NTLM", StringComparison.OrdinalIgnoreCase) ||
+ authType.Equals ("Negotiate", StringComparison.OrdinalIgnoreCase);
+ }
+
+ static async Task DoSendWithAuthAsync (
+ AndroidMessageHandler handler,
+ HttpRequestMessage request,
+ NegotiateAuthentication authContext,
+ RequestedNegotiateAuthenticationData requestedAuth,
+ CancellationToken cancellationToken)
+ {
+ HttpResponseMessage? response = null;
+
+ int requestCounter = 0;
+ string? challengeData = null;
+
+ while (requestCounter++ < MaxRequests) {
+ var challengeResponse = authContext.GetOutgoingBlob (challengeData, out var statusCode);
+
+ if (challengeResponse is null || statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded) {
+ // Response indicated denial even after login, so stop processing and return current response.
+ break;
+ }
+
+ if (response is not null) {
+ // We need to drain the content otherwise the next request
+ // won't reuse the same TCP socket and persistent auth won't work.
+ await response.Content.LoadIntoBufferAsync ().ConfigureAwait (false);
+ }
+
+ SetAuthorizationHeader (request, requestedAuth, challengeResponse);
+ response = await handler.DoSendAsync (request, cancellationToken).ConfigureAwait (false);
+
+ if (authContext.IsAuthenticated || !TryGetChallenge (response, requestedAuth, out challengeData)) {
+ break;
+ }
+
+ if (!IsAuthenticationChallenge (response, requestedAuth))
+ {
+ // Tail response for Negotiate on successful authentication. Validate it before we proceed.
+ authContext.GetOutgoingBlob(challengeData, out statusCode);
+ if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded)
+ {
+ throw new HttpRequestException($"Authentication validation failed with error - {statusCode}.", null, HttpStatusCode.Unauthorized);
+ }
+ break;
+ }
+ }
+
+ return response;
+ }
+
+ static async Task GetTargetName (
+ AndroidMessageHandler handler,
+ HttpRequestMessage request,
+ bool isProxyAuth,
+ CancellationToken cancellationToken)
+ {
+ var hostName = await GetHostName (handler, request, isProxyAuth, cancellationToken);
+ return $"HTTP/{hostName}";
+ }
+
+ static async Task GetHostName (
+ AndroidMessageHandler handler,
+ HttpRequestMessage request,
+ bool isProxyAuth,
+ CancellationToken cancellationToken)
+ {
+ // Calculate SPN (Service Principal Name) using the host name of the request.
+ // Use the request's 'Host' header if available. Otherwise, use the request uri.
+ // Ignore the 'Host' header if this is proxy authentication since we need to use
+ // the host name of the proxy itself for SPN calculation.
+ if (!isProxyAuth && request.Headers.Host != null) {
+ // Use the host name without any normalization.
+ return request.Headers.Host;
+ }
+
+ var requestUri = request.RequestUri!;
+ var authUri = isProxyAuth ? handler.Proxy?.GetProxy (requestUri) ?? requestUri : requestUri;
+
+ // Need to use FQDN normalized host so that CNAME's are traversed.
+ // Use DNS to do the forward lookup to an A (host) record.
+ // But skip DNS lookup on IP literals. Otherwise, we would end up
+ // doing an unintended reverse DNS lookup.
+ if (authUri.HostNameType == UriHostNameType.IPv6 || authUri.HostNameType == UriHostNameType.IPv4) {
+ return authUri.IdnHost;
+ } else {
+ IPHostEntry result = await Dns.GetHostEntryAsync (authUri.IdnHost, cancellationToken).ConfigureAwait (false);
+ return result.HostName;
+ }
+ }
+
+ static void SetAuthorizationHeader (HttpRequestMessage request, RequestedNegotiateAuthenticationData requestedAuth, string challengeResponse)
+ {
+ var headerValue = new AuthenticationHeaderValue (requestedAuth.AuthType, challengeResponse);
+ if (requestedAuth.IsProxyAuth) {
+ request.Headers.ProxyAuthorization = headerValue;
+ } else {
+ request.Headers.Authorization = headerValue;
+ }
+ }
+
+ static bool TryGetChallenge (HttpResponseMessage? response, RequestedNegotiateAuthenticationData requestedAuth, [NotNullWhen (true)] out string? challengeData)
+ {
+ challengeData = null;
+
+ var responseHeaderValues = requestedAuth.IsProxyAuth ? response?.Headers.ProxyAuthenticate : response?.Headers.WwwAuthenticate;
+ if (responseHeaderValues is not null) {
+ foreach (var headerValue in responseHeaderValues) {
+ if (headerValue.Scheme == requestedAuth.AuthType) {
+ challengeData = headerValue.Parameter;
+ break;
+ }
+ }
+ }
+
+ return !string.IsNullOrEmpty (challengeData);
+ }
+
+ static bool IsAuthenticationChallenge (HttpResponseMessage response, RequestedNegotiateAuthenticationData requestedAuth)
+ => requestedAuth.IsProxyAuth
+ ? response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired
+ : response.StatusCode == HttpStatusCode.Unauthorized;
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets
index baa52a204bc..f0a621a1f45 100644
--- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets
@@ -104,6 +104,7 @@
false
+ false
True
diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets
index 1f49b68de85..e7f1364f766 100644
--- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets
@@ -30,6 +30,14 @@ See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f
<_RuntimeConfigReservedProperties Include="APP_CONTEXT_BASE_DIRECTORY"/>
+
+
+
+
+
+
diff --git a/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj
index 22d9dce4925..984363d65ee 100644
--- a/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj
+++ b/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj
@@ -19,6 +19,7 @@
<_MonoAndroidTestPackage>Mono.Android.NET_Tests
-$(TestsFlavor)NET6
IL2037
+ true