File: System\Net\Http\SocketsHttpHandler\AuthenticationHelper.NtAuth.cs
Web Access
Project: src\src\libraries\System.Net.Http\src\System.Net.Http.csproj (System.Net.Http)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net.Http
{
    internal static partial class AuthenticationHelper
    {
        private const string UsePortInSpnCtxSwitch = "System.Net.Http.UsePortInSpn";
        private const string UsePortInSpnEnvironmentVariable = "DOTNET_SYSTEM_NET_HTTP_USEPORTINSPN";
 
        private static volatile int s_usePortInSpn = -1;
 
        private static bool UsePortInSpn
        {
            get
            {
                int usePortInSpn = s_usePortInSpn;
                if (usePortInSpn != -1)
                {
                    return usePortInSpn != 0;
                }
 
                // First check for the AppContext switch, giving it priority over the environment variable.
                if (AppContext.TryGetSwitch(UsePortInSpnCtxSwitch, out bool value))
                {
                    s_usePortInSpn = value ? 1 : 0;
                }
                else
                {
                    // AppContext switch wasn't used. Check the environment variable.
                    s_usePortInSpn =
                        Environment.GetEnvironmentVariable(UsePortInSpnEnvironmentVariable) is string envVar &&
                        (envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase)) ? 1 : 0;
                }
 
                return s_usePortInSpn != 0;
            }
        }
 
        private static Task<HttpResponseMessage> InnerSendAsync(HttpRequestMessage request, bool async, bool isProxyAuth, HttpConnectionPool pool, HttpConnection connection, CancellationToken cancellationToken)
        {
            return isProxyAuth ?
                connection.SendAsync(request, async, cancellationToken) :
                pool.SendWithNtProxyAuthAsync(connection, request, async, cancellationToken);
        }
 
        private static bool ProxySupportsConnectionAuth(HttpResponseMessage response)
        {
            if (!response.Headers.TryGetValues(KnownHeaders.ProxySupport.Descriptor, out IEnumerable<string>? values))
            {
                return false;
            }
 
            foreach (string v in values)
            {
                if (v.Equals("Session-Based-Authentication", StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, TokenImpersonationLevel impersonationLevel, bool isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
        {
            HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false);
            if (!isProxyAuth && connection.Kind == HttpConnectionKind.Proxy && !ProxySupportsConnectionAuth(response))
            {
                // Proxy didn't indicate that it supports connection-based auth, so we can't proceed.
                if (NetEventSource.Log.IsEnabled())
                {
                    NetEventSource.Error(connection, $"Proxy doesn't support connection-based auth, uri={authUri}");
                }
                return response;
            }
 
            if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge challenge))
            {
                if (challenge.AuthenticationType == AuthenticationType.Negotiate ||
                    challenge.AuthenticationType == AuthenticationType.Ntlm)
                {
                    bool isNewConnection = false;
                    bool needDrain = true;
                    try
                    {
                        if (response.Headers.ConnectionClose.GetValueOrDefault())
                        {
                            // Server is closing the connection and asking us to authenticate on a new connection.
 
                            // First, detach the current connection from the pool. This means it will no longer count against the connection limit.
                            // Instead, it will be replaced by the new connection below.
                            connection.DetachFromPool();
 
                            connection = await connectionPool.CreateHttp11ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false);
                            connection!.Acquire();
                            isNewConnection = true;
                            needDrain = false;
                        }
 
                        if (NetEventSource.Log.IsEnabled())
                        {
                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Uri: {authUri.AbsoluteUri}");
                        }
 
                        // 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.HasHeaders && request.Headers.Host != null)
                        {
                            // Use the host name without any normalization.
                            hostName = request.Headers.Host;
                            if (NetEventSource.Log.IsEnabled())
                            {
                                NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: {hostName}");
                            }
                        }
                        else
                        {
                            // 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.
                            UriHostNameType hnt = authUri.HostNameType;
                            if (hnt == UriHostNameType.IPv6 || hnt == UriHostNameType.IPv4)
                            {
                                hostName = authUri.IdnHost;
                            }
                            else
                            {
                                IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost, cancellationToken).ConfigureAwait(false);
                                hostName = result.HostName;
                            }
 
                            if (!isProxyAuth && !authUri.IsDefaultPort && UsePortInSpn)
                            {
                                hostName = string.Create(null, stackalloc char[128], $"{hostName}:{authUri.Port}");
                            }
                        }
 
                        string spn = "HTTP/" + hostName;
                        if (NetEventSource.Log.IsEnabled())
                        {
                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, SPN: {spn}");
                        }
 
                        ProtectionLevel requiredProtectionLevel = ProtectionLevel.None;
                        // 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. On macOS we always need to enforce the integrity
                        // to avoid the GSSAPI implementation generating corrupted authentication
                        // tokens.
                        if (!isProxyAuth || OperatingSystem.IsMacOS())
                        {
                            requiredProtectionLevel = ProtectionLevel.Sign;
                        }
 
                        NegotiateAuthenticationClientOptions authClientOptions = new NegotiateAuthenticationClientOptions
                        {
                            Package = challenge.SchemeName,
                            Credential = challenge.Credential,
                            TargetName = spn,
                            RequiredProtectionLevel = requiredProtectionLevel,
                            Binding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint),
                            AllowedImpersonationLevel = impersonationLevel
                        };
 
                        using NegotiateAuthentication authContext = new NegotiateAuthentication(authClientOptions);
                        string? challengeData = challenge.ChallengeData;
                        NegotiateAuthenticationStatusCode statusCode;
                        while (true)
                        {
                            string? challengeResponse = authContext.GetOutgoingBlob(challengeData, out statusCode);
                            if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded || challengeResponse == null)
                            {
                                // Response indicated denial even after login, so stop processing and return current response.
                                break;
                            }
 
                            if (needDrain)
                            {
                                await connection.DrainResponseAsync(response!, cancellationToken).ConfigureAwait(false);
                            }
 
                            SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(challenge.SchemeName, challengeResponse), isProxyAuth);
 
                            response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false);
                            if (authContext.IsAuthenticated || !TryGetChallengeDataForScheme(challenge.SchemeName, GetResponseAuthenticationHeaderValues(response, isProxyAuth), out challengeData))
                            {
                                break;
                            }
 
                            if (!IsAuthenticationChallenge(response, isProxyAuth))
                            {
                                // Tail response for Negotiate on successful authentication. Validate it before we proceed.
                                authContext.GetOutgoingBlob(challengeData, out statusCode);
                                if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded)
                                {
                                    isNewConnection = false;
                                    connection.Dispose();
                                    throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.Format(SR.net_http_authvalidationfailure, statusCode), statusCode: HttpStatusCode.Unauthorized);
                                }
                                break;
                            }
 
                            needDrain = true;
                        }
                    }
                    finally
                    {
                        if (isNewConnection)
                        {
                            connection!.Release();
                        }
                    }
                }
            }
 
            return response!;
        }
 
        public static Task<HttpResponseMessage> SendWithNtProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool async, ICredentials proxyCredentials, TokenImpersonationLevel impersonationLevel, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
        {
            return SendWithNtAuthAsync(request, proxyUri, async, proxyCredentials, impersonationLevel, isProxyAuth: true, connection, connectionPool, cancellationToken);
        }
 
        public static Task<HttpResponseMessage> SendWithNtConnectionAuthAsync(HttpRequestMessage request, bool async, ICredentials credentials, TokenImpersonationLevel impersonationLevel, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
        {
            Debug.Assert(request.RequestUri != null);
            return SendWithNtAuthAsync(request, request.RequestUri, async, credentials, impersonationLevel, isProxyAuth: false, connection, connectionPool, cancellationToken);
        }
    }
}