< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 614
Coverable lines: 614
Total lines: 1058
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 332
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Diagnostics;
 5using System.Diagnostics.CodeAnalysis;
 6using System.Net.Http.Headers;
 7using System.Text;
 8using System.Threading;
 9using System.Threading.Tasks;
 10
 11namespace System.Net.Http
 12{
 13    internal static partial class AuthenticationHelper
 14    {
 15        private const string BasicScheme = "Basic";
 16        private const string DigestScheme = "Digest";
 17        private const string NtlmScheme = "NTLM";
 18        private const string NegotiateScheme = "Negotiate";
 19
 20        private enum AuthenticationType
 21        {
 22            Basic,
 23            Digest,
 24            Ntlm,
 25            Negotiate
 26        }
 27
 28        private readonly struct AuthenticationChallenge
 29        {
 030            public AuthenticationType AuthenticationType { get; }
 031            public string SchemeName { get; }
 032            public NetworkCredential Credential { get; }
 033            public string? ChallengeData { get; }
 34
 35            public AuthenticationChallenge(AuthenticationType authenticationType, string schemeName, NetworkCredential c
 036            {
 037                AuthenticationType = authenticationType;
 038                SchemeName = schemeName;
 039                Credential = credential;
 040                ChallengeData = challenge;
 041            }
 42        }
 43
 44        private static bool TryGetChallengeDataForScheme(string scheme, HttpHeaderValueCollection<AuthenticationHeaderVa
 045        {
 046            foreach (AuthenticationHeaderValue ahv in authenticationHeaderValues)
 047            {
 048                if (StringComparer.OrdinalIgnoreCase.Equals(scheme, ahv.Scheme))
 049                {
 50                    // Note, a valid challenge can have challengeData == null
 051                    challengeData = ahv.Parameter;
 052                    return true;
 53                }
 054            }
 55
 056            challengeData = null;
 057            return false;
 058        }
 59
 60        // Helper function to determine if response is part of session-based authentication challenge.
 61        internal static bool IsSessionAuthenticationChallenge(HttpResponseMessage response)
 062        {
 063            if (response.StatusCode != HttpStatusCode.Unauthorized)
 064            {
 065                return false;
 66            }
 67
 068            HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues = GetResponseAuthenticationH
 069            foreach (AuthenticationHeaderValue ahv in authenticationHeaderValues)
 070            {
 071                if (StringComparer.OrdinalIgnoreCase.Equals(NegotiateScheme, ahv.Scheme) || StringComparer.OrdinalIgnore
 072                {
 073                    return true;
 74                }
 075            }
 76
 077            return false;
 078        }
 79
 80        private static bool TryGetValidAuthenticationChallengeForScheme(string scheme, AuthenticationType authentication
 81            HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues, out AuthenticationChallenge
 082        {
 083            challenge = default;
 84
 085            if (!TryGetChallengeDataForScheme(scheme, authenticationHeaderValues, out string? challengeData))
 086            {
 087                return false;
 88            }
 89
 090            NetworkCredential? credential = credentials.GetCredential(uri, scheme);
 091            if (credential == null)
 092            {
 93                // We have no credential for this auth type, so we can't respond to the challenge.
 94                // We'll continue to look for a different auth type that we do have a credential for.
 095                if (NetEventSource.Log.IsEnabled())
 096                {
 097                    NetEventSource.AuthenticationInfo(uri, $"Authentication scheme '{scheme}' supported by server, but n
 098                }
 099                return false;
 100            }
 101
 0102            challenge = new AuthenticationChallenge(authenticationType, scheme, credential, challengeData);
 0103            if (NetEventSource.Log.IsEnabled())
 0104            {
 0105                NetEventSource.AuthenticationInfo(uri, $"Authentication scheme '{scheme}' selected. Client username={cha
 0106            }
 0107            return true;
 0108        }
 109
 110        private static bool TryGetAuthenticationChallenge(HttpResponseMessage response, bool isProxyAuth, Uri authUri, I
 0111        {
 0112            if (!IsAuthenticationChallenge(response, isProxyAuth))
 0113            {
 0114                challenge = default;
 0115                return false;
 116            }
 117
 118            // Try to get a valid challenge for the schemes we support, in priority order.
 0119            HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues = GetResponseAuthenticationH
 0120            if (NetEventSource.Log.IsEnabled())
 0121            {
 0122                NetEventSource.AuthenticationInfo(authUri, $"{(isProxyAuth ? "Proxy" : "Server")} authentication request
 0123            }
 0124            return
 0125                TryGetValidAuthenticationChallengeForScheme(NegotiateScheme, AuthenticationType.Negotiate, authUri, cred
 0126                TryGetValidAuthenticationChallengeForScheme(NtlmScheme, AuthenticationType.Ntlm, authUri, credentials, a
 0127                TryGetValidAuthenticationChallengeForScheme(DigestScheme, AuthenticationType.Digest, authUri, credential
 0128                TryGetValidAuthenticationChallengeForScheme(BasicScheme, AuthenticationType.Basic, authUri, credentials,
 0129        }
 130
 131        private static bool TryGetRepeatedChallenge(HttpResponseMessage response, string scheme, bool isProxyAuth, out s
 0132        {
 0133            challengeData = null;
 134
 0135            if (!IsAuthenticationChallenge(response, isProxyAuth))
 0136            {
 0137                return false;
 138            }
 139
 0140            if (!TryGetChallengeDataForScheme(scheme, GetResponseAuthenticationHeaderValues(response, isProxyAuth), out 
 0141            {
 142                // We got another challenge status code, but couldn't find the challenge for the scheme we're handling c
 143                // Just stop processing auth.
 0144                return false;
 145            }
 146
 0147            return true;
 0148        }
 149
 150        private static bool IsAuthenticationChallenge(HttpResponseMessage response, bool isProxyAuth)
 0151        {
 0152            return isProxyAuth ?
 0153                response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired :
 0154                response.StatusCode == HttpStatusCode.Unauthorized;
 0155        }
 156
 157        private static HttpHeaderValueCollection<AuthenticationHeaderValue> GetResponseAuthenticationHeaderValues(HttpRe
 0158        {
 0159            return isProxyAuth ?
 0160                response.Headers.ProxyAuthenticate :
 0161                response.Headers.WwwAuthenticate;
 0162        }
 163
 164        private static void SetRequestAuthenticationHeaderValue(HttpRequestMessage request, AuthenticationHeaderValue he
 0165        {
 0166            if (isProxyAuth)
 0167            {
 0168                request.Headers.ProxyAuthorization = headerValue;
 0169            }
 170            else
 0171            {
 0172                request.Headers.Authorization = headerValue;
 0173            }
 0174        }
 175
 176        private static void SetBasicAuthToken(HttpRequestMessage request, NetworkCredential credential, bool isProxyAuth
 0177        {
 0178            string authString = !string.IsNullOrEmpty(credential.Domain) ?
 0179                credential.Domain + "\\" + credential.UserName + ":" + credential.Password :
 0180                credential.UserName + ":" + credential.Password;
 181
 0182            string base64AuthString = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
 183
 0184            SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(BasicScheme, base64AuthString), i
 0185        }
 186
 187        private static async ValueTask<bool> TrySetDigestAuthToken(HttpRequestMessage request, NetworkCredential credent
 0188        {
 0189            string? parameter = await GetDigestTokenForCredential(credential, request, digestResponse).ConfigureAwait(fa
 190
 191            // Any errors in obtaining parameter return false and we don't proceed with auth
 0192            if (string.IsNullOrEmpty(parameter))
 0193            {
 0194                if (NetEventSource.Log.IsEnabled())
 0195                {
 0196                    NetEventSource.AuthenticationError(request.RequestUri, $"Unable to find 'Digest' authentication toke
 0197                }
 0198                return false;
 199            }
 200
 0201            var headerValue = new AuthenticationHeaderValue(DigestScheme, parameter);
 0202            SetRequestAuthenticationHeaderValue(request, headerValue, isProxyAuth);
 0203            return true;
 0204        }
 205
 206        private static ValueTask<HttpResponseMessage> InnerSendAsync(HttpRequestMessage request, bool async, bool isProx
 0207        {
 0208            return isProxyAuth ?
 0209                pool.SendWithVersionDetectionAndRetryAsync(request, async, doRequestAuth, cancellationToken) :
 0210                pool.SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken);
 0211        }
 212
 213        private static async ValueTask<HttpResponseMessage> SendWithAuthAsync(HttpRequestMessage request, Uri authUri, b
 0214        {
 215            // If preauth is enabled, try to set a Basic auth header proactively on the first request.
 216            // Currently we only support preauth for Basic.
 0217            NetworkCredential? preAuthCredential = null;
 0218            Uri? preAuthCredentialUri = null;
 0219            if (preAuthenticate)
 0220            {
 0221                if (isProxyAuth)
 0222                {
 223                    // For proxy pre-authentication, get Basic credentials directly from the
 224                    // supplied proxy credentials. This is needed for proxies that don't send 407
 225                    // challenges but instead drop or reject unauthenticated connections.
 0226                    NetworkCredential? credential = credentials.GetCredential(authUri, BasicScheme);
 0227                    if (credential != null && credential != CredentialCache.DefaultNetworkCredentials)
 0228                    {
 0229                        preAuthCredential = credential;
 0230                        SetBasicAuthToken(request, credential, isProxyAuth: true);
 0231                    }
 0232                }
 233                else
 0234                {
 235                    // For request pre-authentication, look up credentials from the preauth cache.
 0236                    Debug.Assert(pool.PreAuthCredentials != null);
 237                    (Uri uriPrefix, NetworkCredential credential)? preAuthCredentialPair;
 0238                    lock (pool.PreAuthCredentials)
 0239                    {
 240                        // Just look for basic credentials.  If in the future we support preauth
 241                        // for other schemes, this will need to search in order of precedence.
 0242                        Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NegotiateScheme) == null);
 0243                        Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NtlmScheme) == null);
 0244                        Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, DigestScheme) == null);
 0245                        preAuthCredentialPair = pool.PreAuthCredentials.GetCredential(authUri, BasicScheme);
 0246                    }
 247
 0248                    if (preAuthCredentialPair != null)
 0249                    {
 0250                        (preAuthCredentialUri, preAuthCredential) = preAuthCredentialPair.Value;
 0251                        SetBasicAuthToken(request, preAuthCredential, isProxyAuth);
 0252                    }
 0253                }
 0254            }
 255
 0256            HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancel
 257
 0258            if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge c
 0259            {
 0260                switch (challenge.AuthenticationType)
 261                {
 262                    case AuthenticationType.Digest:
 0263                        if (CredentialCache.DefaultCredentials == credentials)
 0264                        {
 265                            // The DefaultCredentials applies only to NTLM, negotiate, and Kerberos-based authentication
 0266                            break;
 267                        }
 268
 0269                        var digestResponse = new DigestResponse(challenge.ChallengeData);
 0270                        if (await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAuth).Conf
 0271                        {
 0272                            response.Dispose();
 0273                            response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellati
 274
 275                            // Retry in case of nonce timeout in server.
 0276                            if (TryGetRepeatedChallenge(response, challenge.SchemeName, isProxyAuth, out string? challen
 0277                            {
 0278                                digestResponse = new DigestResponse(challengeData);
 0279                                if (IsServerNonceStale(digestResponse) &&
 0280                                    await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAu
 0281                                {
 0282                                    response.Dispose();
 0283                                    response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, ca
 0284                                }
 0285                            }
 0286                        }
 0287                        break;
 288
 289                    case AuthenticationType.Basic:
 0290                        if (CredentialCache.DefaultCredentials == credentials)
 0291                        {
 292                            // The DefaultCredentials applies only to NTLM, negotiate, and Kerberos-based authentication
 0293                            break;
 294                        }
 295
 0296                        if (preAuthCredential != null)
 0297                        {
 0298                            if (NetEventSource.Log.IsEnabled())
 0299                            {
 0300                                NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyAuth ? "p
 0301                            }
 302
 0303                            if (challenge.Credential == preAuthCredential)
 0304                            {
 305                                // Pre auth failed, and user supplied credentials are still same, we can stop there.
 0306                                break;
 307                            }
 308
 309                            // Pre-auth credentials have changed, continue with the new ones.
 310                            // The old ones will be removed below.
 0311                        }
 312
 0313                        response.Dispose();
 0314                        SetBasicAuthToken(request, challenge.Credential, isProxyAuth);
 0315                        response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationTo
 316
 0317                        if (preAuthenticate && !isProxyAuth)
 0318                        {
 0319                            switch (response.StatusCode)
 320                            {
 321                                case HttpStatusCode.ProxyAuthenticationRequired:
 322                                case HttpStatusCode.Unauthorized:
 0323                                    if (NetEventSource.Log.IsEnabled())
 0324                                    {
 0325                                        NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyA
 0326                                    }
 0327                                    break;
 328
 329                                default:
 0330                                    lock (pool.PreAuthCredentials!)
 0331                                    {
 332                                        // remove previously cached (failing) creds
 0333                                        if (preAuthCredentialUri != null)
 0334                                        {
 0335                                            if (NetEventSource.Log.IsEnabled())
 0336                                            {
 0337                                                NetEventSource.Info(pool.PreAuthCredentials, $"Removing Basic credential
 0338                                            }
 339
 0340                                            pool.PreAuthCredentials.Remove(preAuthCredentialUri, BasicScheme);
 0341                                        }
 342
 343                                        try
 0344                                        {
 0345                                            if (NetEventSource.Log.IsEnabled())
 0346                                            {
 0347                                                NetEventSource.Info(pool.PreAuthCredentials, $"Adding Basic credential t
 0348                                            }
 0349                                            pool.PreAuthCredentials.Add(authUri, BasicScheme, challenge.Credential);
 0350                                        }
 0351                                        catch (ArgumentException)
 0352                                        {
 353                                            // The credential already existed.
 0354                                            if (NetEventSource.Log.IsEnabled())
 0355                                            {
 0356                                                NetEventSource.Info(pool.PreAuthCredentials, $"Basic credential present 
 0357                                            }
 0358                                        }
 0359                                    }
 0360                                    break;
 361                            }
 0362                        }
 0363                        break;
 364                }
 0365            }
 366
 0367            if (NetEventSource.Log.IsEnabled() && response.StatusCode == HttpStatusCode.Unauthorized)
 0368            {
 0369                NetEventSource.AuthenticationError(authUri, $"{(isProxyAuth ? "Proxy" : "Server")} authentication failed
 0370            }
 371
 0372            return response;
 0373        }
 374
 375        public static ValueTask<HttpResponseMessage> SendWithProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bo
 0376        {
 0377            return SendWithAuthAsync(request, proxyUri, async, proxyCredentials, preAuthenticate: GlobalHttpSettings.Soc
 0378        }
 379
 380        public static ValueTask<HttpResponseMessage> SendWithRequestAuthAsync(HttpRequestMessage request, bool async, IC
 0381        {
 0382            Debug.Assert(request.RequestUri != null);
 0383            return SendWithAuthAsync(request, request.RequestUri, async, credentials, preAuthenticate, isProxyAuth: fals
 0384        }
 385    }
 386}
 387

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.Digest.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.IO;
 7using System.Net;
 8using System.Net.Http.Headers;
 9using System.Security.Cryptography;
 10using System.Text;
 11using System.Threading.Tasks;
 12
 13namespace System.Net.Http
 14{
 15    internal static partial class AuthenticationHelper
 16    {
 17        // Define digest constants
 18        private const string Qop = "qop";
 19        private const string Auth = "auth";
 20        private const string AuthInt = "auth-int";
 21        private const string Domain = "domain";
 22        private const string Nonce = "nonce";
 23        private const string NC = "nc";
 24        private const string Realm = "realm";
 25        private const string UserHash = "userhash";
 26        private const string Username = "username";
 27        private const string UsernameStar = "username*";
 28        private const string Algorithm = "algorithm";
 29        private const string Uri = "uri";
 30        private const string Sha256 = "SHA-256";
 31        private const string Md5 = "MD5";
 32        private const string Sha256Sess = "SHA-256-sess";
 33        private const string MD5Sess = "MD5-sess";
 34        private const string CNonce = "cnonce";
 35        private const string Opaque = "opaque";
 36        private const string Response = "response";
 37        private const string Stale = "stale";
 38
 39        public static async Task<string?> GetDigestTokenForCredential(NetworkCredential credential, HttpRequestMessage r
 040        {
 041            StringBuilder sb = StringBuilderCache.Acquire();
 42
 43            // It is mandatory for servers to implement sha-256 per RFC 7616
 44            // Keep MD5 for backward compatibility.
 45            string? algorithm;
 046            bool isAlgorithmSpecified = digestResponse.Parameters.TryGetValue(Algorithm, out algorithm);
 047            if (isAlgorithmSpecified)
 048            {
 049                if (!algorithm!.Equals(Sha256, StringComparison.OrdinalIgnoreCase) &&
 050                    !algorithm.Equals(Md5, StringComparison.OrdinalIgnoreCase) &&
 051                    !algorithm.Equals(Sha256Sess, StringComparison.OrdinalIgnoreCase) &&
 052                    !algorithm.Equals(MD5Sess, StringComparison.OrdinalIgnoreCase))
 053                {
 054                    if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(digestResponse, $"Algorithm not supported: 
 055                    return null;
 56                }
 057            }
 58            else
 059            {
 060                algorithm = Md5;
 061            }
 62
 63            // Check if nonce is there in challenge
 64            string? nonce;
 065            if (!digestResponse.Parameters.TryGetValue(Nonce, out nonce))
 066            {
 067                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(digestResponse, "Nonce missing");
 068                return null;
 69            }
 70
 71            // opaque token may or may not exist
 72            string? opaque;
 073            digestResponse.Parameters.TryGetValue(Opaque, out opaque);
 74
 75            string? realm;
 076            if (!digestResponse.Parameters.TryGetValue(Realm, out realm))
 077            {
 078                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(digestResponse, "Realm missing");
 079                return null;
 80            }
 81
 82            // Add username
 83            string? userhash;
 084            if (digestResponse.Parameters.TryGetValue(UserHash, out userhash) && userhash == "true")
 085            {
 086                sb.AppendKeyValue(Username, ComputeHash(credential.UserName + ":" + realm, algorithm));
 087                sb.AppendKeyValue(UserHash, userhash, includeQuotes: false);
 088            }
 89            else
 090            {
 091                if (!Ascii.IsValid(credential.UserName))
 092                {
 093                    string usernameStar = HeaderUtilities.Encode5987(credential.UserName);
 094                    sb.AppendKeyValue(UsernameStar, usernameStar, includeQuotes: false);
 095                }
 96                else
 097                {
 098                    sb.AppendKeyValue(Username, credential.UserName);
 099                }
 0100            }
 101
 102            // Add realm
 0103            sb.AppendKeyValue(Realm, realm);
 104
 105            // Add nonce
 0106            sb.AppendKeyValue(Nonce, nonce);
 107
 0108            Debug.Assert(request.RequestUri != null);
 109            // Add uri
 0110            sb.AppendKeyValue(Uri, request.RequestUri.PathAndQuery);
 111
 112            // Set qop, default is auth
 0113            string qop = Auth;
 0114            bool isQopSpecified = digestResponse.Parameters.ContainsKey(Qop);
 0115            if (isQopSpecified)
 0116            {
 117                // Check if auth-int present in qop string
 0118                int index1 = digestResponse.Parameters[Qop].IndexOf(AuthInt, StringComparison.Ordinal);
 0119                if (index1 != -1)
 0120                {
 121                    // Get index of auth if present in qop string
 0122                    int index2 = digestResponse.Parameters[Qop].IndexOf(Auth, StringComparison.Ordinal);
 123
 124                    // If index2 < index1, auth option is available
 125                    // If index2 == index1, check if auth option available later in string after auth-int.
 0126                    if (index2 == index1)
 0127                    {
 0128                        index2 = digestResponse.Parameters[Qop].IndexOf(Auth, index1 + AuthInt.Length, StringComparison.
 0129                        if (index2 == -1)
 0130                        {
 0131                            qop = AuthInt;
 0132                        }
 0133                    }
 0134                }
 0135            }
 136
 137            // Set cnonce
 0138            string cnonce = GetRandomAlphaNumericString();
 139
 140            // Calculate response
 0141            string a1 = credential.UserName + ":" + realm + ":" + credential.Password;
 0142            if (algorithm.EndsWith("sess", StringComparison.OrdinalIgnoreCase))
 0143            {
 0144                a1 = ComputeHash(a1, algorithm) + ":" + nonce + ":" + cnonce;
 0145            }
 146
 0147            string a2 = request.Method.Method + ":" + request.RequestUri.PathAndQuery;
 0148            if (qop == AuthInt)
 0149            {
 0150                string content = request.Content == null ? string.Empty : await request.Content.ReadAsStringAsync().Conf
 0151                a2 = a2 + ":" + ComputeHash(content, algorithm);
 0152            }
 153
 154            string response;
 0155            if (isQopSpecified)
 0156            {
 0157                response = ComputeHash(ComputeHash(a1, algorithm) + ":" +
 0158                                            nonce + ":" +
 0159                                            DigestResponse.NonceCount + ":" +
 0160                                            cnonce + ":" +
 0161                                            qop + ":" +
 0162                                            ComputeHash(a2, algorithm), algorithm);
 0163            }
 164            else
 0165            {
 0166                response = ComputeHash(ComputeHash(a1, algorithm) + ":" +
 0167                            nonce + ":" +
 0168                            ComputeHash(a2, algorithm), algorithm);
 0169            }
 170
 171            // Add response
 0172            sb.AppendKeyValue(Response, response, includeComma: opaque != null || isAlgorithmSpecified || isQopSpecified
 173
 174            // Add opaque
 0175            if (opaque != null)
 0176            {
 0177                sb.AppendKeyValue(Opaque, opaque, includeComma: isAlgorithmSpecified || isQopSpecified);
 0178            }
 179
 0180            if (isAlgorithmSpecified)
 0181            {
 182                // Add algorithm
 0183                sb.AppendKeyValue(Algorithm, algorithm, includeQuotes: false, includeComma: isQopSpecified);
 0184            }
 185
 0186            if (isQopSpecified)
 0187            {
 188                // Add qop
 0189                sb.AppendKeyValue(Qop, qop, includeQuotes: false);
 190
 191                // Add nc
 0192                sb.AppendKeyValue(NC, DigestResponse.NonceCount, includeQuotes: false);
 193
 194                // Add cnonce
 0195                sb.AppendKeyValue(CNonce, cnonce, includeComma: false);
 0196            }
 197
 0198            return StringBuilderCache.GetStringAndRelease(sb);
 0199        }
 200
 201        public static bool IsServerNonceStale(DigestResponse digestResponse)
 0202        {
 0203            return digestResponse.Parameters.TryGetValue(Stale, out string? stale) && stale == "true";
 0204        }
 205
 206        private static string GetRandomAlphaNumericString()
 0207        {
 208            const int Length = 16;
 209            const string CharacterSet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
 0210            return RandomNumberGenerator.GetString(CharacterSet, Length);
 0211        }
 212
 213        private static unsafe string ComputeHash(string data, string algorithm)
 0214        {
 0215            Span<byte> hashBuffer = stackalloc byte[SHA256.HashSizeInBytes]; // SHA256 is the largest hash produced
 0216            byte[] dataBytes = Encoding.UTF8.GetBytes(data);
 217            int written;
 218
 0219            if (algorithm.StartsWith(Sha256, StringComparison.OrdinalIgnoreCase))
 0220            {
 0221                written = SHA256.HashData(dataBytes, hashBuffer);
 0222                Debug.Assert(written == SHA256.HashSizeInBytes);
 0223            }
 224            else
 0225            {
 226                // Disable MD5 insecure warning.
 227#pragma warning disable CA5351
 0228                written = MD5.HashData(dataBytes, hashBuffer);
 0229                Debug.Assert(written == MD5.HashSizeInBytes);
 230#pragma warning restore CA5351
 0231            }
 232
 0233            return Convert.ToHexStringLower(hashBuffer.Slice(0, written));
 0234        }
 235
 236        internal sealed class DigestResponse
 237        {
 0238            internal readonly Dictionary<string, string> Parameters = new Dictionary<string, string>(StringComparer.Ordi
 239            internal const string NonceCount = "00000001";
 240
 0241            internal DigestResponse(string? challenge)
 0242            {
 0243                if (!string.IsNullOrEmpty(challenge))
 0244                    Parse(challenge);
 0245            }
 246
 247            private static bool CharIsSpaceOrTab(char ch)
 0248            {
 0249                return ch == ' ' || ch == '\t';
 0250            }
 251
 252            private static bool MustValueBeQuoted(string key)
 0253            {
 254                // As per the RFC, these string must be quoted for historical reasons.
 0255                return key.Equals(Realm, StringComparison.OrdinalIgnoreCase) || key.Equals(Nonce, StringComparison.Ordin
 0256                    key.Equals(Opaque, StringComparison.OrdinalIgnoreCase) || key.Equals(Qop, StringComparison.OrdinalIg
 0257            }
 258
 259            private static string? GetNextKey(string data, int currentIndex, out int parsedIndex)
 0260            {
 261                // Skip leading space or tab.
 0262                while (currentIndex < data.Length && CharIsSpaceOrTab(data[currentIndex]))
 0263                {
 0264                    currentIndex++;
 0265                }
 266
 267                // Start parsing key
 0268                int start = currentIndex;
 269
 270                // Parse till '=' is encountered marking end of key.
 271                // Key cannot contain space or tab, break if either is found.
 0272                while (currentIndex < data.Length && data[currentIndex] != '=' && !CharIsSpaceOrTab(data[currentIndex]))
 0273                {
 0274                    currentIndex++;
 0275                }
 276
 0277                if (currentIndex == data.Length)
 0278                {
 279                    // Key didn't terminate with '='
 0280                    parsedIndex = currentIndex;
 0281                    return null;
 282                }
 283
 284                // Record end of key.
 0285                int length = currentIndex - start;
 0286                if (CharIsSpaceOrTab(data[currentIndex]))
 0287                {
 288                    // Key parsing terminated due to ' ' or '\t'.
 289                    // Parse till '=' is found.
 0290                    while (currentIndex < data.Length && CharIsSpaceOrTab(data[currentIndex]))
 0291                    {
 0292                        currentIndex++;
 0293                    }
 294
 0295                    if (currentIndex == data.Length || data[currentIndex] != '=')
 0296                    {
 297                        // Key is invalid.
 0298                        parsedIndex = currentIndex;
 0299                        return null;
 300                    }
 0301                }
 302
 303                // Skip trailing space and tab and '='
 0304                while (currentIndex < data.Length && (CharIsSpaceOrTab(data[currentIndex]) || data[currentIndex] == '=')
 0305                {
 0306                    currentIndex++;
 0307                }
 308
 309                // Set the parsedIndex to current valid char.
 0310                parsedIndex = currentIndex;
 0311                return data.Substring(start, length);
 0312            }
 313
 314            private static string? GetNextValue(string data, int currentIndex, bool expectQuotes, out int parsedIndex)
 0315            {
 0316                Debug.Assert(currentIndex < data.Length && !CharIsSpaceOrTab(data[currentIndex]));
 317
 318                // If quoted value, skip first quote.
 0319                bool quotedValue = false;
 0320                if (data[currentIndex] == '"')
 0321                {
 0322                    quotedValue = true;
 0323                    currentIndex++;
 0324                }
 325
 0326                if (expectQuotes && !quotedValue)
 0327                {
 0328                    parsedIndex = currentIndex;
 0329                    return null;
 330                }
 331
 0332                StringBuilder sb = StringBuilderCache.Acquire();
 0333                while (currentIndex < data.Length && ((quotedValue && data[currentIndex] != '"') || (!quotedValue && dat
 0334                {
 0335                    sb.Append(data[currentIndex]);
 0336                    currentIndex++;
 337
 0338                    if (currentIndex == data.Length)
 0339                        break;
 340
 0341                    if (!quotedValue && CharIsSpaceOrTab(data[currentIndex]))
 0342                        break;
 343
 0344                    if (quotedValue && data[currentIndex] == '"' && data[currentIndex - 1] == '\\')
 0345                    {
 346                        // Include the escaped quote.
 0347                        sb.Append(data[currentIndex]);
 0348                        currentIndex++;
 0349                    }
 0350                }
 351
 352                // Skip the quote.
 0353                if (quotedValue)
 0354                    currentIndex++;
 355
 356                // Skip any whitespace.
 0357                while (currentIndex < data.Length && CharIsSpaceOrTab(data[currentIndex]))
 0358                    currentIndex++;
 359
 360                // Return if this is last value.
 0361                if (currentIndex == data.Length)
 0362                {
 0363                    parsedIndex = currentIndex;
 0364                    return StringBuilderCache.GetStringAndRelease(sb);
 365                }
 366
 367                // A key-value pair should end with ','
 0368                if (data[currentIndex++] != ',')
 0369                {
 0370                    parsedIndex = currentIndex;
 0371                    return null;
 372                }
 373
 374                // Skip space and tab
 0375                while (currentIndex < data.Length && CharIsSpaceOrTab(data[currentIndex]))
 0376                {
 0377                    currentIndex++;
 0378                }
 379
 380                // Set parsedIndex to current valid char.
 0381                parsedIndex = currentIndex;
 0382                return StringBuilderCache.GetStringAndRelease(sb);
 0383            }
 384
 385            private void Parse(string challenge)
 0386            {
 0387                int parsedIndex = 0;
 0388                while (parsedIndex < challenge.Length)
 0389                {
 390                    // Get the key.
 0391                    string? key = GetNextKey(challenge, parsedIndex, out parsedIndex);
 392                    // Ensure key is not empty and parsedIndex is still in range.
 0393                    if (string.IsNullOrEmpty(key) || parsedIndex >= challenge.Length)
 0394                        break;
 395
 396                    // Get the value.
 0397                    string? value = GetNextValue(challenge, parsedIndex, MustValueBeQuoted(key), out parsedIndex);
 0398                    if (value == null)
 0399                        break;
 400
 401                    // Ensure value is valid.
 402                    // Opaque, Domain and Realm can have empty string
 0403                    if (value == string.Empty &&
 0404                        !key.Equals(Opaque, StringComparison.OrdinalIgnoreCase) &&
 0405                        !key.Equals(Domain, StringComparison.OrdinalIgnoreCase) &&
 0406                        !key.Equals(Realm, StringComparison.OrdinalIgnoreCase))
 0407                        break;
 408
 409                    // Add the key-value pair to Parameters.
 0410                    Parameters.Add(key, value);
 0411                }
 0412            }
 413        }
 414    }
 415
 416    internal static class StringBuilderExtensions
 417    {
 418        public static void AppendKeyValue(this StringBuilder sb, string key, string value, bool includeQuotes = true, bo
 419        {
 420            sb.Append(key).Append('=');
 421
 422            if (includeQuotes)
 423            {
 424                ReadOnlySpan<char> valueSpan = value;
 425                sb.Append('"');
 426                while (true)
 427                {
 428                    int i = valueSpan.IndexOfAny('"', '\\'); // Characters that require escaping in quoted string
 429                    if (i >= 0)
 430                    {
 431                        sb.Append(valueSpan.Slice(0, i)).Append('\\').Append(valueSpan[i]);
 432                        valueSpan = valueSpan.Slice(i + 1);
 433                    }
 434                    else
 435                    {
 436                        sb.Append(valueSpan);
 437                        break;
 438                    }
 439                }
 440                sb.Append('"');
 441            }
 442            else
 443            {
 444                sb.Append(value);
 445            }
 446
 447            if (includeComma)
 448            {
 449                sb.Append(',').Append(' ');
 450            }
 451        }
 452    }
 453}
 454

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Generic;
 5using System.ComponentModel;
 6using System.Diagnostics;
 7using System.Net;
 8using System.Net.Http.Headers;
 9using System.Net.Security;
 10using System.Security.Authentication.ExtendedProtection;
 11using System.Security.Principal;
 12using System.Threading;
 13using System.Threading.Tasks;
 14
 15namespace System.Net.Http
 16{
 17    internal static partial class AuthenticationHelper
 18    {
 019        private static bool UsePortInSpn => LocalAppContextSwitches.UsePortInSpn;
 20
 21        private static Task<HttpResponseMessage> InnerSendAsync(HttpRequestMessage request, bool async, bool isProxyAuth
 022        {
 023            return isProxyAuth ?
 024                connection.SendAsync(request, async, cancellationToken) :
 025                pool.SendWithNtProxyAuthAsync(connection, request, async, cancellationToken);
 026        }
 27
 28        private static bool ProxySupportsConnectionAuth(HttpResponseMessage response)
 029        {
 030            if (!response.Headers.TryGetValues(KnownHeaders.ProxySupport.Descriptor, out IEnumerable<string>? values))
 031            {
 032                return false;
 33            }
 34
 035            foreach (string v in values)
 036            {
 037                if (v.Equals("Session-Based-Authentication", StringComparison.OrdinalIgnoreCase))
 038                {
 039                    return true;
 40                }
 041            }
 42
 043            return false;
 044        }
 45
 46        private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, bool
 047        {
 048            HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection,
 049            if (!isProxyAuth && connection.Kind == HttpConnectionKind.Proxy && !ProxySupportsConnectionAuth(response))
 050            {
 51                // Proxy didn't indicate that it supports connection-based auth, so we can't proceed.
 052                if (NetEventSource.Log.IsEnabled())
 053                {
 054                    NetEventSource.Error(connection, $"Proxy doesn't support connection-based auth, uri={authUri}");
 055                }
 056                return response;
 57            }
 58
 059            if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge c
 060            {
 061                if (challenge.AuthenticationType == AuthenticationType.Negotiate ||
 062                    challenge.AuthenticationType == AuthenticationType.Ntlm)
 063                {
 064                    bool isNewConnection = false;
 065                    bool needDrain = true;
 66                    try
 067                    {
 068                        if (response.Headers.ConnectionClose.GetValueOrDefault())
 069                        {
 70                            // Server is closing the connection and asking us to authenticate on a new connection.
 71
 72                            // First, detach the current connection from the pool. This means it will no longer count ag
 73                            // Instead, it will be replaced by the new connection below.
 074                            connection.DetachFromPool();
 75
 076                            connection = await connectionPool.CreateHttp11ConnectionAsync(request, async, cancellationTo
 077                            connection!.Acquire();
 078                            isNewConnection = true;
 079                            needDrain = false;
 080                        }
 81
 082                        if (NetEventSource.Log.IsEnabled())
 083                        {
 084                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Uri: {auth
 085                        }
 86
 87                        // Calculate SPN (Service Principal Name) using the host name of the request.
 88                        // Use the request's 'Host' header if available. Otherwise, use the request uri.
 89                        // Ignore the 'Host' header if this is proxy authentication since we need to use
 90                        // the host name of the proxy itself for SPN calculation.
 91                        string hostName;
 092                        if (!isProxyAuth && request.HasHeaders && request.Headers.Host != null)
 093                        {
 94                            // Use the host name without any normalization.
 095                            hostName = request.Headers.Host;
 096                            if (NetEventSource.Log.IsEnabled())
 097                            {
 098                                NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: 
 099                            }
 0100                        }
 101                        else
 0102                        {
 103                            // Need to use FQDN normalized host so that CNAME's are traversed.
 104                            // Use DNS to do the forward lookup to an A (host) record.
 105                            // But skip DNS lookup on IP literals. Otherwise, we would end up
 106                            // doing an unintended reverse DNS lookup.
 0107                            UriHostNameType hnt = authUri.HostNameType;
 0108                            if (hnt == UriHostNameType.IPv6 || hnt == UriHostNameType.IPv4)
 0109                            {
 0110                                hostName = authUri.IdnHost;
 0111                            }
 112                            else
 0113                            {
 0114                                IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost, cancellationToken).Con
 0115                                hostName = result.HostName;
 0116                            }
 117
 0118                            if (!isProxyAuth && !authUri.IsDefaultPort && UsePortInSpn)
 0119                            {
 0120                                hostName = string.Create(null, stackalloc char[128], $"{hostName}:{authUri.Port}");
 0121                            }
 0122                        }
 123
 0124                        string spn = "HTTP/" + hostName;
 0125                        if (NetEventSource.Log.IsEnabled())
 0126                        {
 0127                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, SPN: {spn}
 0128                        }
 129
 0130                        ProtectionLevel requiredProtectionLevel = ProtectionLevel.None;
 131                        // When connecting to proxy server don't enforce the integrity to avoid
 132                        // compatibility issues. The assumption is that the proxy server comes
 133                        // from a trusted source. On macOS we always need to enforce the integrity
 134                        // to avoid the GSSAPI implementation generating corrupted authentication
 135                        // tokens.
 0136                        if (!isProxyAuth || OperatingSystem.IsMacOS())
 0137                        {
 0138                            requiredProtectionLevel = ProtectionLevel.Sign;
 0139                        }
 140
 0141                        NegotiateAuthenticationClientOptions authClientOptions = new NegotiateAuthenticationClientOption
 0142                        {
 0143                            Package = challenge.SchemeName,
 0144                            Credential = challenge.Credential,
 0145                            TargetName = spn,
 0146                            RequiredProtectionLevel = requiredProtectionLevel,
 0147                            Binding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint),
 0148                            AllowedImpersonationLevel = impersonationLevel
 0149                        };
 150
 0151                        using NegotiateAuthentication authContext = new NegotiateAuthentication(authClientOptions);
 0152                        string? challengeData = challenge.ChallengeData;
 153                        NegotiateAuthenticationStatusCode statusCode;
 0154                        while (true)
 0155                        {
 0156                            string? challengeResponse = authContext.GetOutgoingBlob(challengeData, out statusCode);
 0157                            if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded || challengeResponse == nu
 0158                            {
 159                                // Response indicated denial even after login, so stop processing and return current res
 0160                                break;
 161                            }
 162
 0163                            if (needDrain)
 0164                            {
 0165                                await connection.DrainResponseAsync(response!, cancellationToken).ConfigureAwait(false);
 0166                            }
 167
 0168                            SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(challenge.SchemeN
 169
 0170                            response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, can
 0171                            if (authContext.IsAuthenticated || !TryGetChallengeDataForScheme(challenge.SchemeName, GetRe
 0172                            {
 0173                                break;
 174                            }
 175
 0176                            if (!IsAuthenticationChallenge(response, isProxyAuth))
 0177                            {
 178                                // Tail response for Negotiate on successful authentication. Validate it before we proce
 0179                                authContext.GetOutgoingBlob(challengeData, out statusCode);
 0180                                if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded)
 0181                                {
 0182                                    isNewConnection = false;
 0183                                    connection.Dispose();
 0184                                    throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.Format(S
 185                                }
 0186                                break;
 187                            }
 188
 0189                            needDrain = true;
 0190                        }
 0191                    }
 192                    finally
 0193                    {
 0194                        if (isNewConnection)
 0195                        {
 0196                            connection!.Release();
 0197                        }
 0198                    }
 0199                }
 0200            }
 201
 0202            return response!;
 0203        }
 204
 205        public static Task<HttpResponseMessage> SendWithNtProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool 
 0206        {
 0207            return SendWithNtAuthAsync(request, proxyUri, async, proxyCredentials, impersonationLevel, isProxyAuth: true
 0208        }
 209
 210        public static Task<HttpResponseMessage> SendWithNtConnectionAuthAsync(HttpRequestMessage request, bool async, IC
 0211        {
 0212            Debug.Assert(request.RequestUri != null);
 0213            return SendWithNtAuthAsync(request, request.RequestUri, async, credentials, impersonationLevel, isProxyAuth:
 0214        }
 215    }
 216}
 217

Methods/Properties

AuthenticationType()
SchemeName()
Credential()
ChallengeData()
.ctor(System.Net.Http.AuthenticationHelper/AuthenticationType,System.String,System.Net.NetworkCredential,System.String)
TryGetChallengeDataForScheme(System.String,System.Net.Http.Headers.HttpHeaderValueCollection`1<System.Net.Http.Headers.AuthenticationHeaderValue>,System.String&)
IsSessionAuthenticationChallenge(System.Net.Http.HttpResponseMessage)
TryGetValidAuthenticationChallengeForScheme(System.String,System.Net.Http.AuthenticationHelper/AuthenticationType,System.Uri,System.Net.ICredentials,System.Net.Http.Headers.HttpHeaderValueCollection`1<System.Net.Http.Headers.AuthenticationHeaderValue>,System.Net.Http.AuthenticationHelper/AuthenticationChallenge&)
TryGetAuthenticationChallenge(System.Net.Http.HttpResponseMessage,System.Boolean,System.Uri,System.Net.ICredentials,System.Net.Http.AuthenticationHelper/AuthenticationChallenge&)
TryGetRepeatedChallenge(System.Net.Http.HttpResponseMessage,System.String,System.Boolean,System.String&)
IsAuthenticationChallenge(System.Net.Http.HttpResponseMessage,System.Boolean)
GetResponseAuthenticationHeaderValues(System.Net.Http.HttpResponseMessage,System.Boolean)
SetRequestAuthenticationHeaderValue(System.Net.Http.HttpRequestMessage,System.Net.Http.Headers.AuthenticationHeaderValue,System.Boolean)
SetBasicAuthToken(System.Net.Http.HttpRequestMessage,System.Net.NetworkCredential,System.Boolean)
TrySetDigestAuthToken(System.Net.Http.HttpRequestMessage,System.Net.NetworkCredential,System.Net.Http.AuthenticationHelper/DigestResponse,System.Boolean)
InnerSendAsync(System.Net.Http.HttpRequestMessage,System.Boolean,System.Boolean,System.Boolean,System.Net.Http.HttpConnectionPool,System.Threading.CancellationToken)
SendWithAuthAsync(System.Net.Http.HttpRequestMessage,System.Uri,System.Boolean,System.Net.ICredentials,System.Boolean,System.Boolean,System.Boolean,System.Net.Http.HttpConnectionPool,System.Threading.CancellationToken)
SendWithProxyAuthAsync(System.Net.Http.HttpRequestMessage,System.Uri,System.Boolean,System.Net.ICredentials,System.Boolean,System.Net.Http.HttpConnectionPool,System.Threading.CancellationToken)
SendWithRequestAuthAsync(System.Net.Http.HttpRequestMessage,System.Boolean,System.Net.ICredentials,System.Boolean,System.Net.Http.HttpConnectionPool,System.Threading.CancellationToken)
GetDigestTokenForCredential(System.Net.NetworkCredential,System.Net.Http.HttpRequestMessage,System.Net.Http.AuthenticationHelper/DigestResponse)
IsServerNonceStale(System.Net.Http.AuthenticationHelper/DigestResponse)
GetRandomAlphaNumericString()
ComputeHash(System.String,System.String)
.ctor(System.String)
CharIsSpaceOrTab(System.Char)
MustValueBeQuoted(System.String)
GetNextKey(System.String,System.Int32,System.Int32&)
GetNextValue(System.String,System.Int32,System.Boolean,System.Int32&)
Parse(System.String)
UsePortInSpn()
InnerSendAsync(System.Net.Http.HttpRequestMessage,System.Boolean,System.Boolean,System.Net.Http.HttpConnectionPool,System.Net.Http.HttpConnection,System.Threading.CancellationToken)
ProxySupportsConnectionAuth(System.Net.Http.HttpResponseMessage)
SendWithNtAuthAsync(System.Net.Http.HttpRequestMessage,System.Uri,System.Boolean,System.Net.ICredentials,System.Security.Principal.TokenImpersonationLevel,System.Boolean,System.Net.Http.HttpConnection,System.Net.Http.HttpConnectionPool,System.Threading.CancellationToken)
SendWithNtProxyAuthAsync(System.Net.Http.HttpRequestMessage,System.Uri,System.Boolean,System.Net.ICredentials,System.Security.Principal.TokenImpersonationLevel,System.Net.Http.HttpConnection,System.Net.Http.HttpConnectionPool,System.Threading.CancellationToken)
SendWithNtConnectionAuthAsync(System.Net.Http.HttpRequestMessage,System.Boolean,System.Net.ICredentials,System.Security.Principal.TokenImpersonationLevel,System.Net.Http.HttpConnection,System.Net.Http.HttpConnectionPool,System.Threading.CancellationToken)