Skip to content

Commit

Permalink
Refactor NTAuthenticationHelper into NTAuthenticationHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
simonrozsival committed May 31, 2022
1 parent 76d488e commit a4a1868
Show file tree
Hide file tree
Showing 7 changed files with 426 additions and 284 deletions.
3 changes: 1 addition & 2 deletions src/Mono.Android/Mono.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,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\NTAuthenticationHandler.cs" />
<Compile Condition=" '$(TargetFramework)' == 'monoandroid10' " Include="Xamarin.Android.Net\OldAndroidSSLSocketFactory.cs" />
</ItemGroup>

Expand Down
30 changes: 27 additions & 3 deletions src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,33 @@ public CookieContainer CookieContainer

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

private bool CouldHaveNTCredentials => Proxy != null || Credentials != null;
#if !MONOANDROID1_0
IWebProxy? _proxy;
ICredentials? _credentials;
NTAuthenticationHandler.Helper? _ntAuthHelper;

public IWebProxy? Proxy
{
get => _proxy;
set {
_proxy = value;
_ntAuthHelper ??= new NTAuthenticationHandler.Helper (this);
}
}

public ICredentials? Credentials
{
get => _credentials;
set {
_credentials = value;
_ntAuthHelper ??= new NTAuthenticationHandler.Helper (this);
}
}
#else
public IWebProxy? Proxy { get; set; }

public ICredentials? Credentials { get; set; }
#endif

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

Expand Down Expand Up @@ -338,8 +360,10 @@ string EncodeUrl (Uri url)
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);
if (RequestNeedsAuthorization && _ntAuthHelper != null && _ntAuthHelper.RequestNeedsNTAuthentication (request, out var ntAuth)) {
var authenticatedResponse = await ntAuth.ResendRequestWithAuthAsync (cancellationToken).ConfigureAwait (false);
if (authenticatedResponse != null)
return authenticatedResponse;
}
#endif

Expand Down
281 changes: 281 additions & 0 deletions src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
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.Reflection;
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 sealed class NTAuthenticationHandler
{
const int MaxRequests = 10;

internal sealed class Helper
{
readonly AndroidMessageHandler _handler;

internal Helper (AndroidMessageHandler handler)
{
_handler = handler;
}

internal bool RequestNeedsNTAuthentication (HttpRequestMessage request, [NotNullWhen (true)] out NTAuthenticationHandler? ntAuthHandler)
{
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;
var correspondingCredentials = credentials?.GetCredential (request.RequestUri, authType);

if (correspondingCredentials != null) {
ntAuthHandler = new NTAuthenticationHandler (_handler, request, authType, auth.UseProxyAuthentication, correspondingCredentials);
return true;
}
}
}

ntAuthHandler = null;
return false;
}

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

readonly AndroidMessageHandler _handler;
readonly HttpRequestMessage _request;
readonly string _authType;
readonly bool _isProxyAuth;
readonly NetworkCredential _credentials;

private NTAuthenticationHandler (
AndroidMessageHandler handler,
HttpRequestMessage request,
string authType,
bool isProxyAuth,
NetworkCredential credentials)
{
_handler = handler;
_request = request;
_authType = authType;
_isProxyAuth = isProxyAuth;
_credentials = credentials;
}

internal async Task <HttpResponseMessage?> ResendRequestWithAuthAsync (CancellationToken cancellationToken)
{
var authContext = new NTAuthentication (
isServer: false,
_authType,
_credentials,
spn: await GetSpn (cancellationToken).ConfigureAwait (false),
requestedContextFlags: GetRequestedContextFlags (),
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 {
return await DoResendRequestWithAuthAsync (authContext, cancellationToken);
} finally {
_handler.PreAuthenticate = originalPreAuthenticate;
authContext.CloseContext ();
}
}

async Task <HttpResponseMessage?> DoResendRequestWithAuthAsync (NTAuthentication authContext, CancellationToken cancellationToken)
{
HttpResponseMessage? response = null;

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 (_isProxyAuth) {
_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, out challenge)) {
break;
}

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

return response;
}

async Task<string> GetSpn (CancellationToken cancellationToken)
{
var hostName = await GetHostName (cancellationToken);
return $"HTTP/{hostName}";
}

async Task<string> GetHostName (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;
}
}

int GetRequestedContextFlags ()
{
// 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;
}

bool TryGetChallenge (HttpResponseMessage response, [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);
}

bool IsAuthenticationChallenge (HttpResponseMessage response)
=> _isProxyAuth
? response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired
: response.StatusCode == HttpStatusCode.Unauthorized;

private sealed class NTAuthentication
{
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 NTAuthentication (
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) ?? false);

[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);
}
}
}
Loading

0 comments on commit a4a1868

Please sign in to comment.