Skip to content

Commit

Permalink
Add initial implementation of NT Authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
simonrozsival committed Jun 8, 2022
1 parent 6b43af3 commit f856a57
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Mono.Android/Mono.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@
<Compile Include="Xamarin.Android.Net\AuthModuleBasic.cs" />
<Compile Include="Xamarin.Android.Net\AuthModuleDigest.cs" />
<Compile Include="Xamarin.Android.Net\IAndroidAuthenticationModule.cs" />
<Compile Include="Xamarin.Android.Net\NTAuthenticationHelper.cs" />
<Compile Include="Xamarin.Android.Net\NTAuthenticationProxy.cs" />
<Compile Include="Xamarin.Android.Net\X509TrustManagerWithValidationCallback.cs" />
<Compile Condition=" '$(TargetFramework)' == 'monoandroid10' " Include="Xamarin.Android.Net\OldAndroidSSLSocketFactory.cs" />
</ItemGroup>
Expand Down
15 changes: 15 additions & 0 deletions src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public CookieContainer CookieContainer

public bool UseProxy { get; set; } = true;

private bool CouldHaveNTCredentials => Proxy != null || Credentials != null;

public IWebProxy? Proxy { get; set; }

public ICredentials? Credentials { get; set; }
Expand Down Expand Up @@ -332,6 +334,19 @@ string EncodeUrl (Uri url)
/// <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)
{
var response = await DoSendAsync (request, cancellationToken).ConfigureAwait (false);

#if !MONOANDROID1_0
if (CouldHaveNTCredentials && RequestNeedsAuthorization && NTAuthenticationHelper.TryGetSupportedAuthMethod (this, request, out var auth, out var credentials)) {
response = await NTAuthenticationHelper.SendAsync (this, request, response, auth, credentials, cancellationToken).ConfigureAwait (false);
}
#endif

return response;
}

internal async Task <HttpResponseMessage> DoSendAsync (HttpRequestMessage request, CancellationToken cancellationToken)
{
AssertSelf ();
if (request == null)
Expand Down
193 changes: 193 additions & 0 deletions src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Authentication.ExtendedProtection;
using System.Threading;
using System.Threading.Tasks;

namespace Xamarin.Android.Net
{
// This code is heavily inspired by System.Net.Http.AuthenticationHelper
internal static class NTAuthenticationHelper
{
const int MaxRequests = 10;

internal static bool TryGetSupportedAuthMethod (
AndroidMessageHandler handler,
HttpRequestMessage request,
[NotNullWhen (true)] out AuthenticationData? supportedAuth,
[NotNullWhen (true)] out NetworkCredential? suitableCredentials)
{
IEnumerable<AuthenticationData> requestedAuthentication = handler.RequestedAuthentication ?? Enumerable.Empty<AuthenticationData> ();
foreach (var auth in requestedAuthentication) {
if (TryGetSupportedAuthType (auth.Challenge, out var authType)) {
var credentials = auth.UseProxyAuthentication ? handler.Proxy?.Credentials : handler.Credentials;
suitableCredentials = credentials?.GetCredential (request.RequestUri, authType);

if (suitableCredentials != null) {
supportedAuth = auth;
return true;
}
}
}

supportedAuth = null;
suitableCredentials = null;
return false;
}

internal static async Task <HttpResponseMessage> SendAsync (
AndroidMessageHandler handler,
HttpRequestMessage request,
HttpResponseMessage response,
AuthenticationData auth,
NetworkCredential credentials,
CancellationToken cancellationToken)
{
var authType = GetSupportedAuthType (auth.Challenge);
var isProxyAuth = auth.UseProxyAuthentication;
var authContext = new NTAuthenticationProxy (
isServer: false,
authType,
credentials,
spn: await GetSpn (handler, request, isProxyAuth, cancellationToken).ConfigureAwait (false),
requestedContextFlags: GetRequestedContextFlags (isProxyAuth),
channelBinding: null);

// 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 {
string? challenge = null;
int requestCounter = 0;

while (requestCounter++ < MaxRequests) {
var challengeResponse = authContext.GetOutgoingBlob (challenge);
if (challengeResponse == null) {
// response indicated denial even after login, so stop processing
// and return current response
break;
}

var headerValue = new AuthenticationHeaderValue (authType, challengeResponse);
if (auth.UseProxyAuthentication) {
request.Headers.ProxyAuthorization = headerValue;
} else {
request.Headers.Authorization = headerValue;
}

response = await handler.DoSendAsync (request, cancellationToken).ConfigureAwait (false);

// 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);

if (authContext.IsCompleted || !TryGetChallenge (response, authType, isProxyAuth, out challenge)) {
break;
}

if (!IsAuthenticationChallenge (response, isProxyAuth)) {
// Tail response for Negotiate on successful authentication.
// Validate it before we proceed.
authContext.GetOutgoingBlob (challenge);
break;
}
}

return response;
} finally {
handler.PreAuthenticate = originalPreAuthenticate;
authContext.CloseContext ();
}
}

static string GetSupportedAuthType (string challenge)
{
if (!TryGetSupportedAuthType (challenge, out var authType)) {
throw new InvalidOperationException ($"Authenticaton scheme {authType} is not supported by {nameof (NTAuthenticationHelper)}.");
}

return authType;
}

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<string?> GetSpn (
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.
string hostName;
if (!isProxyAuth && request.Headers.Host != null) {
// Use the host name without any normalization.
hostName = request.Headers.Host;
} else {
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) {
hostName = authUri.IdnHost;
} else {
IPHostEntry result = await Dns.GetHostEntryAsync (authUri.IdnHost, cancellationToken).ConfigureAwait (false);
hostName = result.HostName;
}
}

return $"HTTP/{hostName}";
}

static int GetRequestedContextFlags (bool isProxyAuth)
{
// the ContextFlagsPal is internal type in dotnet/runtime and we can't
// use it directly here so we have to use ints directly
int contextFlags = 0x00000800; // ContextFlagsPal.Connection

// When connecting to proxy server don't enforce the integrity to avoid
// compatibility issues. The assumption is that the proxy server comes
// from a trusted source.
if (!isProxyAuth) {
contextFlags |= 0x00010000; // ContextFlagsPal.InitIntegrity
}

return contextFlags;
}

static bool TryGetChallenge (
HttpResponseMessage response,
string authType,
bool isProxyAuth,
[NotNullWhen (true)] out string? challenge)
{
var responseHeaderValues = isProxyAuth ? response.Headers.ProxyAuthenticate : response.Headers.WwwAuthenticate;
challenge = responseHeaderValues?.FirstOrDefault (headerValue => headerValue.Scheme == authType)?.Parameter;
return !string.IsNullOrEmpty (challenge);
}

static bool IsAuthenticationChallenge (HttpResponseMessage response, bool isProxyAuth)
=> isProxyAuth
? response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired
: response.StatusCode == HttpStatusCode.Unauthorized;
}
}
86 changes: 86 additions & 0 deletions src/Mono.Android/Xamarin.Android.Net/NTAuthenticationProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Reflection;
using System.Runtime;
using System.Security.Authentication.ExtendedProtection;

namespace Xamarin.Android.Net
{
internal sealed class NTAuthenticationProxy
{
const string AssemblyName = "System.Net.Http";
const string TypeName = "System.Net.NTAuthentication";
const string ContextFlagsPalTypeName = "System.Net.ContextFlagsPal";

const string ConstructorDescription = "#ctor(System.Boolean,System.String,System.Net.NetworkCredential,System.String,System.Net.ContextFlagsPal,System.Security.Authentication.ExtendedProtection.ChannelBinding)";
const string IsCompletedPropertyName = "IsCompleted";
const string GetOutgoingBlobMethodName = "GetOutgoingBlob";
const string CloseContextMethodName = "CloseContext";

const BindingFlags InstanceBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

static Lazy<Type> s_NTAuthenticationType = new (() => FindType (TypeName, AssemblyName));
static Lazy<ConstructorInfo> s_NTAuthenticationConstructorInfo = new (() => GetNTAuthenticationConstructor ());
static Lazy<PropertyInfo> s_IsCompletedPropertyInfo = new (() => GetProperty (IsCompletedPropertyName));
static Lazy<MethodInfo> s_GetOutgoingBlobMethodInfo = new (() => GetMethod (GetOutgoingBlobMethodName));
static Lazy<MethodInfo> s_CloseContextMethodInfo = new (() => GetMethod (CloseContextMethodName));

static Type FindType (string typeName, string assemblyName)
=> Type.GetType ($"{typeName}, {assemblyName}", throwOnError: true)!;

static ConstructorInfo GetNTAuthenticationConstructor ()
{
var contextFlagsPalType = FindType (ContextFlagsPalTypeName, AssemblyName);
var paramTypes = new[] {
typeof (bool),
typeof (string),
typeof (NetworkCredential),
typeof (string),
contextFlagsPalType,
typeof (ChannelBinding)
};

return s_NTAuthenticationType.Value.GetConstructor (InstanceBindingFlags, paramTypes)
?? throw new MissingMemberException (TypeName, ConstructorInfo.ConstructorName);
}

static PropertyInfo GetProperty (string name)
=> s_NTAuthenticationType.Value.GetProperty (name, InstanceBindingFlags)
?? throw new MissingMemberException (TypeName, name);

static MethodInfo GetMethod (string name)
=> s_NTAuthenticationType.Value.GetMethod (name, InstanceBindingFlags)
?? throw new MissingMemberException (TypeName, name);

object _instance;

[DynamicDependency (ConstructorDescription, TypeName, AssemblyName)]
internal NTAuthenticationProxy (
bool isServer,
string package,
NetworkCredential credential,
string? spn,
int requestedContextFlags,
ChannelBinding? channelBinding)
{
var constructorParams = new object?[] { isServer, package, credential, spn, requestedContextFlags, channelBinding };
_instance = s_NTAuthenticationConstructorInfo.Value.Invoke (constructorParams);
}

public bool IsCompleted
=> GetIsCompleted ();

[DynamicDependency ($"get_{IsCompletedPropertyName}", TypeName, AssemblyName)]
bool GetIsCompleted ()
=> (bool)s_IsCompletedPropertyInfo.Value.GetValue (_instance);

[DynamicDependency (GetOutgoingBlobMethodName, TypeName, AssemblyName)]
public string? GetOutgoingBlob (string? incomingBlob)
=> (string?)s_GetOutgoingBlobMethodInfo.Value.Invoke (_instance, new object?[] { incomingBlob });

[DynamicDependency (CloseContextMethodName, TypeName, AssemblyName)]
public void CloseContext ()
=> s_CloseContextMethodInfo.Value.Invoke (_instance, null);
}
}

0 comments on commit f856a57

Please sign in to comment.