File: System\Net\Http\SocketsHttpHandler\AuthenticationHelper.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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net.Http
{
    internal static partial class AuthenticationHelper
    {
        private const string BasicScheme = "Basic";
        private const string DigestScheme = "Digest";
        private const string NtlmScheme = "NTLM";
        private const string NegotiateScheme = "Negotiate";
 
        private enum AuthenticationType
        {
            Basic,
            Digest,
            Ntlm,
            Negotiate
        }
 
        private readonly struct AuthenticationChallenge
        {
            public AuthenticationType AuthenticationType { get; }
            public string SchemeName { get; }
            public NetworkCredential Credential { get; }
            public string? ChallengeData { get; }
 
            public AuthenticationChallenge(AuthenticationType authenticationType, string schemeName, NetworkCredential credential, string? challenge)
            {
                AuthenticationType = authenticationType;
                SchemeName = schemeName;
                Credential = credential;
                ChallengeData = challenge;
            }
        }
 
        private static bool TryGetChallengeDataForScheme(string scheme, HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues, out string? challengeData)
        {
            foreach (AuthenticationHeaderValue ahv in authenticationHeaderValues)
            {
                if (StringComparer.OrdinalIgnoreCase.Equals(scheme, ahv.Scheme))
                {
                    // Note, a valid challenge can have challengeData == null
                    challengeData = ahv.Parameter;
                    return true;
                }
            }
 
            challengeData = null;
            return false;
        }
 
        // Helper function to determine if response is part of session-based authentication challenge.
        internal static bool IsSessionAuthenticationChallenge(HttpResponseMessage response)
        {
            if (response.StatusCode != HttpStatusCode.Unauthorized)
            {
                return false;
            }
 
            HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues = GetResponseAuthenticationHeaderValues(response, isProxyAuth: false);
            foreach (AuthenticationHeaderValue ahv in authenticationHeaderValues)
            {
                if (StringComparer.OrdinalIgnoreCase.Equals(NegotiateScheme, ahv.Scheme) || StringComparer.OrdinalIgnoreCase.Equals(NtlmScheme, ahv.Scheme))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        private static bool TryGetValidAuthenticationChallengeForScheme(string scheme, AuthenticationType authenticationType, Uri uri, ICredentials credentials,
            HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues, out AuthenticationChallenge challenge)
        {
            challenge = default;
 
            if (!TryGetChallengeDataForScheme(scheme, authenticationHeaderValues, out string? challengeData))
            {
                return false;
            }
 
            NetworkCredential? credential = credentials.GetCredential(uri, scheme);
            if (credential == null)
            {
                // We have no credential for this auth type, so we can't respond to the challenge.
                // We'll continue to look for a different auth type that we do have a credential for.
                if (NetEventSource.Log.IsEnabled())
                {
                    NetEventSource.AuthenticationInfo(uri, $"Authentication scheme '{scheme}' supported by server, but not by client.");
                }
                return false;
            }
 
            challenge = new AuthenticationChallenge(authenticationType, scheme, credential, challengeData);
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.AuthenticationInfo(uri, $"Authentication scheme '{scheme}' selected. Client username={challenge.Credential.UserName}");
            }
            return true;
        }
 
        private static bool TryGetAuthenticationChallenge(HttpResponseMessage response, bool isProxyAuth, Uri authUri, ICredentials credentials, out AuthenticationChallenge challenge)
        {
            if (!IsAuthenticationChallenge(response, isProxyAuth))
            {
                challenge = default;
                return false;
            }
 
            // Try to get a valid challenge for the schemes we support, in priority order.
            HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues = GetResponseAuthenticationHeaderValues(response, isProxyAuth);
            if (NetEventSource.Log.IsEnabled())
            {
                NetEventSource.AuthenticationInfo(authUri, $"{(isProxyAuth ? "Proxy" : "Server")} authentication requested with WWW-Authenticate header value '{authenticationHeaderValues}'");
            }
            return
                TryGetValidAuthenticationChallengeForScheme(NegotiateScheme, AuthenticationType.Negotiate, authUri, credentials, authenticationHeaderValues, out challenge) ||
                TryGetValidAuthenticationChallengeForScheme(NtlmScheme, AuthenticationType.Ntlm, authUri, credentials, authenticationHeaderValues, out challenge) ||
                TryGetValidAuthenticationChallengeForScheme(DigestScheme, AuthenticationType.Digest, authUri, credentials, authenticationHeaderValues, out challenge) ||
                TryGetValidAuthenticationChallengeForScheme(BasicScheme, AuthenticationType.Basic, authUri, credentials, authenticationHeaderValues, out challenge);
        }
 
        private static bool TryGetRepeatedChallenge(HttpResponseMessage response, string scheme, bool isProxyAuth, out string? challengeData)
        {
            challengeData = null;
 
            if (!IsAuthenticationChallenge(response, isProxyAuth))
            {
                return false;
            }
 
            if (!TryGetChallengeDataForScheme(scheme, GetResponseAuthenticationHeaderValues(response, isProxyAuth), out challengeData))
            {
                // We got another challenge status code, but couldn't find the challenge for the scheme we're handling currently.
                // Just stop processing auth.
                return false;
            }
 
            return true;
        }
 
        private static bool IsAuthenticationChallenge(HttpResponseMessage response, bool isProxyAuth)
        {
            return isProxyAuth ?
                response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired :
                response.StatusCode == HttpStatusCode.Unauthorized;
        }
 
        private static HttpHeaderValueCollection<AuthenticationHeaderValue> GetResponseAuthenticationHeaderValues(HttpResponseMessage response, bool isProxyAuth)
        {
            return isProxyAuth ?
                response.Headers.ProxyAuthenticate :
                response.Headers.WwwAuthenticate;
        }
 
        private static void SetRequestAuthenticationHeaderValue(HttpRequestMessage request, AuthenticationHeaderValue headerValue, bool isProxyAuth)
        {
            if (isProxyAuth)
            {
                request.Headers.ProxyAuthorization = headerValue;
            }
            else
            {
                request.Headers.Authorization = headerValue;
            }
        }
 
        private static void SetBasicAuthToken(HttpRequestMessage request, NetworkCredential credential, bool isProxyAuth)
        {
            string authString = !string.IsNullOrEmpty(credential.Domain) ?
                credential.Domain + "\\" + credential.UserName + ":" + credential.Password :
                credential.UserName + ":" + credential.Password;
 
            string base64AuthString = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
 
            SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(BasicScheme, base64AuthString), isProxyAuth);
        }
 
        private static async ValueTask<bool> TrySetDigestAuthToken(HttpRequestMessage request, NetworkCredential credential, DigestResponse digestResponse, bool isProxyAuth)
        {
            string? parameter = await GetDigestTokenForCredential(credential, request, digestResponse).ConfigureAwait(false);
 
            // Any errors in obtaining parameter return false and we don't proceed with auth
            if (string.IsNullOrEmpty(parameter))
            {
                if (NetEventSource.Log.IsEnabled())
                {
                    NetEventSource.AuthenticationError(request.RequestUri, $"Unable to find 'Digest' authentication token when authenticating with {(isProxyAuth ? "proxy" : "server")}");
                }
                return false;
            }
 
            var headerValue = new AuthenticationHeaderValue(DigestScheme, parameter);
            SetRequestAuthenticationHeaderValue(request, headerValue, isProxyAuth);
            return true;
        }
 
        private static ValueTask<HttpResponseMessage> InnerSendAsync(HttpRequestMessage request, bool async, bool isProxyAuth, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken)
        {
            return isProxyAuth ?
                pool.SendWithVersionDetectionAndRetryAsync(request, async, doRequestAuth, cancellationToken) :
                pool.SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken);
        }
 
        private static async ValueTask<HttpResponseMessage> SendWithAuthAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, bool preAuthenticate, bool isProxyAuth, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken)
        {
            // If preauth is enabled and this isn't proxy auth, try to get a basic credential from the
            // preauth credentials cache, and if successful, set an auth header for it onto the request.
            // Currently we only support preauth for Basic.
            NetworkCredential? preAuthCredential = null;
            Uri? preAuthCredentialUri = null;
            if (preAuthenticate)
            {
                Debug.Assert(pool.PreAuthCredentials != null);
                (Uri uriPrefix, NetworkCredential credential)? preAuthCredentialPair;
                lock (pool.PreAuthCredentials)
                {
                    // Just look for basic credentials.  If in the future we support preauth
                    // for other schemes, this will need to search in order of precedence.
                    Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NegotiateScheme) == null);
                    Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NtlmScheme) == null);
                    Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, DigestScheme) == null);
                    preAuthCredentialPair = pool.PreAuthCredentials.GetCredential(authUri, BasicScheme);
                }
 
                if (preAuthCredentialPair != null)
                {
                    (preAuthCredentialUri, preAuthCredential) = preAuthCredentialPair.Value;
                    SetBasicAuthToken(request, preAuthCredential, isProxyAuth);
                }
            }
 
            HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false);
 
            if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge challenge))
            {
                switch (challenge.AuthenticationType)
                {
                    case AuthenticationType.Digest:
                        var digestResponse = new DigestResponse(challenge.ChallengeData);
                        if (await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAuth).ConfigureAwait(false))
                        {
                            response.Dispose();
                            response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false);
 
                            // Retry in case of nonce timeout in server.
                            if (TryGetRepeatedChallenge(response, challenge.SchemeName, isProxyAuth, out string? challengeData))
                            {
                                digestResponse = new DigestResponse(challengeData);
                                if (IsServerNonceStale(digestResponse) &&
                                    await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAuth).ConfigureAwait(false))
                                {
                                    response.Dispose();
                                    response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false);
                                }
                            }
                        }
                        break;
 
                    case AuthenticationType.Basic:
                        if (preAuthCredential != null)
                        {
                            if (NetEventSource.Log.IsEnabled())
                            {
                                NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyAuth ? "proxy" : "server")} failed.");
                            }
 
                            if (challenge.Credential == preAuthCredential)
                            {
                                // Pre auth failed, and user supplied credentials are still same, we can stop there.
                                break;
                            }
 
                            // Pre-auth credentials have changed, continue with the new ones.
                            // The old ones will be removed below.
                        }
 
                        response.Dispose();
                        SetBasicAuthToken(request, challenge.Credential, isProxyAuth);
                        response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationToken).ConfigureAwait(false);
 
                        if (preAuthenticate)
                        {
                            switch (response.StatusCode)
                            {
                                case HttpStatusCode.ProxyAuthenticationRequired:
                                case HttpStatusCode.Unauthorized:
                                    if (NetEventSource.Log.IsEnabled())
                                    {
                                        NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyAuth ? "proxy" : "server")} failed.");
                                    }
                                    break;
 
                                default:
                                    lock (pool.PreAuthCredentials!)
                                    {
                                        // remove previously cached (failing) creds
                                        if (preAuthCredentialUri != null)
                                        {
                                            if (NetEventSource.Log.IsEnabled())
                                            {
                                                NetEventSource.Info(pool.PreAuthCredentials, $"Removing Basic credential from cache, uri={preAuthCredentialUri}, username={preAuthCredential!.UserName}");
                                            }
 
                                            pool.PreAuthCredentials.Remove(preAuthCredentialUri, BasicScheme);
                                        }
 
                                        try
                                        {
                                            if (NetEventSource.Log.IsEnabled())
                                            {
                                                NetEventSource.Info(pool.PreAuthCredentials, $"Adding Basic credential to cache, uri={authUri}, username={challenge.Credential.UserName}");
                                            }
                                            pool.PreAuthCredentials.Add(authUri, BasicScheme, challenge.Credential);
                                        }
                                        catch (ArgumentException)
                                        {
                                            // The credential already existed.
                                            if (NetEventSource.Log.IsEnabled())
                                            {
                                                NetEventSource.Info(pool.PreAuthCredentials, $"Basic credential present in cache, uri={authUri}, username={challenge.Credential.UserName}");
                                            }
                                        }
                                    }
                                    break;
                            }
                        }
                        break;
                }
            }
 
            if (NetEventSource.Log.IsEnabled() && response.StatusCode == HttpStatusCode.Unauthorized)
            {
                NetEventSource.AuthenticationError(authUri, $"{(isProxyAuth ? "Proxy" : "Server")} authentication failed.");
            }
 
            return response;
        }
 
        public static ValueTask<HttpResponseMessage> SendWithProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool async, ICredentials proxyCredentials, bool doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken)
        {
            return SendWithAuthAsync(request, proxyUri, async, proxyCredentials, preAuthenticate: false, isProxyAuth: true, doRequestAuth, pool, cancellationToken);
        }
 
        public static ValueTask<HttpResponseMessage> SendWithRequestAuthAsync(HttpRequestMessage request, bool async, ICredentials credentials, bool preAuthenticate, HttpConnectionPool pool, CancellationToken cancellationToken)
        {
            Debug.Assert(request.RequestUri != null);
            return SendWithAuthAsync(request, request.RequestUri, async, credentials, preAuthenticate, isProxyAuth: false, doRequestAuth: true, pool, cancellationToken);
        }
    }
}