< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 619
Coverable lines: 619
Total lines: 1070
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 338
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

D:\runner\runtime\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 and this isn't proxy auth, try to get a basic credential from the
 216            // preauth credentials cache, and if successful, set an auth header for it onto the request.
 217            // Currently we only support preauth for Basic.
 0218            NetworkCredential? preAuthCredential = null;
 0219            Uri? preAuthCredentialUri = null;
 0220            if (preAuthenticate)
 0221            {
 0222                Debug.Assert(pool.PreAuthCredentials != null);
 223                (Uri uriPrefix, NetworkCredential credential)? preAuthCredentialPair;
 0224                lock (pool.PreAuthCredentials)
 0225                {
 226                    // Just look for basic credentials.  If in the future we support preauth
 227                    // for other schemes, this will need to search in order of precedence.
 0228                    Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NegotiateScheme) == null);
 0229                    Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NtlmScheme) == null);
 0230                    Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, DigestScheme) == null);
 0231                    preAuthCredentialPair = pool.PreAuthCredentials.GetCredential(authUri, BasicScheme);
 0232                }
 233
 0234                if (preAuthCredentialPair != null)
 0235                {
 0236                    (preAuthCredentialUri, preAuthCredential) = preAuthCredentialPair.Value;
 0237                    SetBasicAuthToken(request, preAuthCredential, isProxyAuth);
 0238                }
 0239            }
 240
 0241            HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancel
 242
 0243            if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge c
 0244            {
 0245                switch (challenge.AuthenticationType)
 246                {
 247                    case AuthenticationType.Digest:
 0248                        if (CredentialCache.DefaultCredentials == credentials)
 0249                        {
 250                            // The DefaultCredentials applies only to NTLM, negotiate, and Kerberos-based authentication
 0251                            break;
 252                        }
 253
 0254                        var digestResponse = new DigestResponse(challenge.ChallengeData);
 0255                        if (await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAuth).Conf
 0256                        {
 0257                            response.Dispose();
 0258                            response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellati
 259
 260                            // Retry in case of nonce timeout in server.
 0261                            if (TryGetRepeatedChallenge(response, challenge.SchemeName, isProxyAuth, out string? challen
 0262                            {
 0263                                digestResponse = new DigestResponse(challengeData);
 0264                                if (IsServerNonceStale(digestResponse) &&
 0265                                    await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAu
 0266                                {
 0267                                    response.Dispose();
 0268                                    response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, ca
 0269                                }
 0270                            }
 0271                        }
 0272                        break;
 273
 274                    case AuthenticationType.Basic:
 0275                        if (CredentialCache.DefaultCredentials == credentials)
 0276                        {
 277                            // The DefaultCredentials applies only to NTLM, negotiate, and Kerberos-based authentication
 0278                            break;
 279                        }
 280
 0281                        if (preAuthCredential != null)
 0282                        {
 0283                            if (NetEventSource.Log.IsEnabled())
 0284                            {
 0285                                NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyAuth ? "p
 0286                            }
 287
 0288                            if (challenge.Credential == preAuthCredential)
 0289                            {
 290                                // Pre auth failed, and user supplied credentials are still same, we can stop there.
 0291                                break;
 292                            }
 293
 294                            // Pre-auth credentials have changed, continue with the new ones.
 295                            // The old ones will be removed below.
 0296                        }
 297
 0298                        response.Dispose();
 0299                        SetBasicAuthToken(request, challenge.Credential, isProxyAuth);
 0300                        response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationTo
 301
 0302                        if (preAuthenticate)
 0303                        {
 0304                            switch (response.StatusCode)
 305                            {
 306                                case HttpStatusCode.ProxyAuthenticationRequired:
 307                                case HttpStatusCode.Unauthorized:
 0308                                    if (NetEventSource.Log.IsEnabled())
 0309                                    {
 0310                                        NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyA
 0311                                    }
 0312                                    break;
 313
 314                                default:
 0315                                    lock (pool.PreAuthCredentials!)
 0316                                    {
 317                                        // remove previously cached (failing) creds
 0318                                        if (preAuthCredentialUri != null)
 0319                                        {
 0320                                            if (NetEventSource.Log.IsEnabled())
 0321                                            {
 0322                                                NetEventSource.Info(pool.PreAuthCredentials, $"Removing Basic credential
 0323                                            }
 324
 0325                                            pool.PreAuthCredentials.Remove(preAuthCredentialUri, BasicScheme);
 0326                                        }
 327
 328                                        try
 0329                                        {
 0330                                            if (NetEventSource.Log.IsEnabled())
 0331                                            {
 0332                                                NetEventSource.Info(pool.PreAuthCredentials, $"Adding Basic credential t
 0333                                            }
 0334                                            pool.PreAuthCredentials.Add(authUri, BasicScheme, challenge.Credential);
 0335                                        }
 0336                                        catch (ArgumentException)
 0337                                        {
 338                                            // The credential already existed.
 0339                                            if (NetEventSource.Log.IsEnabled())
 0340                                            {
 0341                                                NetEventSource.Info(pool.PreAuthCredentials, $"Basic credential present 
 0342                                            }
 0343                                        }
 0344                                    }
 0345                                    break;
 346                            }
 0347                        }
 0348                        break;
 349                }
 0350            }
 351
 0352            if (NetEventSource.Log.IsEnabled() && response.StatusCode == HttpStatusCode.Unauthorized)
 0353            {
 0354                NetEventSource.AuthenticationError(authUri, $"{(isProxyAuth ? "Proxy" : "Server")} authentication failed
 0355            }
 356
 0357            return response;
 0358        }
 359
 360        public static ValueTask<HttpResponseMessage> SendWithProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bo
 0361        {
 0362            return SendWithAuthAsync(request, proxyUri, async, proxyCredentials, preAuthenticate: false, isProxyAuth: tr
 0363        }
 364
 365        public static ValueTask<HttpResponseMessage> SendWithRequestAuthAsync(HttpRequestMessage request, bool async, IC
 0366        {
 0367            Debug.Assert(request.RequestUri != null);
 0368            return SendWithAuthAsync(request, request.RequestUri, async, credentials, preAuthenticate, isProxyAuth: fals
 0369        }
 370    }
 371}

D:\runner\runtime\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 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}

D:\runner\runtime\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    {
 19        private const string UsePortInSpnCtxSwitch = "System.Net.Http.UsePortInSpn";
 20        private const string UsePortInSpnEnvironmentVariable = "DOTNET_SYSTEM_NET_HTTP_USEPORTINSPN";
 21
 022        private static volatile int s_usePortInSpn = -1;
 23
 24        private static bool UsePortInSpn
 25        {
 26            get
 027            {
 028                int usePortInSpn = s_usePortInSpn;
 029                if (usePortInSpn != -1)
 030                {
 031                    return usePortInSpn != 0;
 32                }
 33
 34                // First check for the AppContext switch, giving it priority over the environment variable.
 035                if (AppContext.TryGetSwitch(UsePortInSpnCtxSwitch, out bool value))
 036                {
 037                    s_usePortInSpn = value ? 1 : 0;
 038                }
 39                else
 040                {
 41                    // AppContext switch wasn't used. Check the environment variable.
 042                    s_usePortInSpn =
 043                        Environment.GetEnvironmentVariable(UsePortInSpnEnvironmentVariable) is string envVar &&
 044                        (envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase)) ? 1 : 0;
 045                }
 46
 047                return s_usePortInSpn != 0;
 048            }
 49        }
 50
 51        private static Task<HttpResponseMessage> InnerSendAsync(HttpRequestMessage request, bool async, bool isProxyAuth
 052        {
 053            return isProxyAuth ?
 054                connection.SendAsync(request, async, cancellationToken) :
 055                pool.SendWithNtProxyAuthAsync(connection, request, async, cancellationToken);
 056        }
 57
 58        private static bool ProxySupportsConnectionAuth(HttpResponseMessage response)
 059        {
 060            if (!response.Headers.TryGetValues(KnownHeaders.ProxySupport.Descriptor, out IEnumerable<string>? values))
 061            {
 062                return false;
 63            }
 64
 065            foreach (string v in values)
 066            {
 067                if (v.Equals("Session-Based-Authentication", StringComparison.OrdinalIgnoreCase))
 068                {
 069                    return true;
 70                }
 071            }
 72
 073            return false;
 074        }
 75
 76        private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, bool
 077        {
 078            HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection,
 079            if (!isProxyAuth && connection.Kind == HttpConnectionKind.Proxy && !ProxySupportsConnectionAuth(response))
 080            {
 81                // Proxy didn't indicate that it supports connection-based auth, so we can't proceed.
 082                if (NetEventSource.Log.IsEnabled())
 083                {
 084                    NetEventSource.Error(connection, $"Proxy doesn't support connection-based auth, uri={authUri}");
 085                }
 086                return response;
 87            }
 88
 089            if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge c
 090            {
 091                if (challenge.AuthenticationType == AuthenticationType.Negotiate ||
 092                    challenge.AuthenticationType == AuthenticationType.Ntlm)
 093                {
 094                    bool isNewConnection = false;
 095                    bool needDrain = true;
 96                    try
 097                    {
 098                        if (response.Headers.ConnectionClose.GetValueOrDefault())
 099                        {
 100                            // Server is closing the connection and asking us to authenticate on a new connection.
 101
 102                            // First, detach the current connection from the pool. This means it will no longer count ag
 103                            // Instead, it will be replaced by the new connection below.
 0104                            connection.DetachFromPool();
 105
 0106                            connection = await connectionPool.CreateHttp11ConnectionAsync(request, async, cancellationTo
 0107                            connection!.Acquire();
 0108                            isNewConnection = true;
 0109                            needDrain = false;
 0110                        }
 111
 0112                        if (NetEventSource.Log.IsEnabled())
 0113                        {
 0114                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Uri: {auth
 0115                        }
 116
 117                        // Calculate SPN (Service Principal Name) using the host name of the request.
 118                        // Use the request's 'Host' header if available. Otherwise, use the request uri.
 119                        // Ignore the 'Host' header if this is proxy authentication since we need to use
 120                        // the host name of the proxy itself for SPN calculation.
 121                        string hostName;
 0122                        if (!isProxyAuth && request.HasHeaders && request.Headers.Host != null)
 0123                        {
 124                            // Use the host name without any normalization.
 0125                            hostName = request.Headers.Host;
 0126                            if (NetEventSource.Log.IsEnabled())
 0127                            {
 0128                                NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: 
 0129                            }
 0130                        }
 131                        else
 0132                        {
 133                            // Need to use FQDN normalized host so that CNAME's are traversed.
 134                            // Use DNS to do the forward lookup to an A (host) record.
 135                            // But skip DNS lookup on IP literals. Otherwise, we would end up
 136                            // doing an unintended reverse DNS lookup.
 0137                            UriHostNameType hnt = authUri.HostNameType;
 0138                            if (hnt == UriHostNameType.IPv6 || hnt == UriHostNameType.IPv4)
 0139                            {
 0140                                hostName = authUri.IdnHost;
 0141                            }
 142                            else
 0143                            {
 0144                                IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost, cancellationToken).Con
 0145                                hostName = result.HostName;
 0146                            }
 147
 0148                            if (!isProxyAuth && !authUri.IsDefaultPort && UsePortInSpn)
 0149                            {
 0150                                hostName = string.Create(null, stackalloc char[128], $"{hostName}:{authUri.Port}");
 0151                            }
 0152                        }
 153
 0154                        string spn = "HTTP/" + hostName;
 0155                        if (NetEventSource.Log.IsEnabled())
 0156                        {
 0157                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, SPN: {spn}
 0158                        }
 159
 0160                        ProtectionLevel requiredProtectionLevel = ProtectionLevel.None;
 161                        // When connecting to proxy server don't enforce the integrity to avoid
 162                        // compatibility issues. The assumption is that the proxy server comes
 163                        // from a trusted source. On macOS we always need to enforce the integrity
 164                        // to avoid the GSSAPI implementation generating corrupted authentication
 165                        // tokens.
 0166                        if (!isProxyAuth || OperatingSystem.IsMacOS())
 0167                        {
 0168                            requiredProtectionLevel = ProtectionLevel.Sign;
 0169                        }
 170
 0171                        NegotiateAuthenticationClientOptions authClientOptions = new NegotiateAuthenticationClientOption
 0172                        {
 0173                            Package = challenge.SchemeName,
 0174                            Credential = challenge.Credential,
 0175                            TargetName = spn,
 0176                            RequiredProtectionLevel = requiredProtectionLevel,
 0177                            Binding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint),
 0178                            AllowedImpersonationLevel = impersonationLevel
 0179                        };
 180
 0181                        using NegotiateAuthentication authContext = new NegotiateAuthentication(authClientOptions);
 0182                        string? challengeData = challenge.ChallengeData;
 183                        NegotiateAuthenticationStatusCode statusCode;
 0184                        while (true)
 0185                        {
 0186                            string? challengeResponse = authContext.GetOutgoingBlob(challengeData, out statusCode);
 0187                            if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded || challengeResponse == nu
 0188                            {
 189                                // Response indicated denial even after login, so stop processing and return current res
 0190                                break;
 191                            }
 192
 0193                            if (needDrain)
 0194                            {
 0195                                await connection.DrainResponseAsync(response!, cancellationToken).ConfigureAwait(false);
 0196                            }
 197
 0198                            SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(challenge.SchemeN
 199
 0200                            response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, can
 0201                            if (authContext.IsAuthenticated || !TryGetChallengeDataForScheme(challenge.SchemeName, GetRe
 0202                            {
 0203                                break;
 204                            }
 205
 0206                            if (!IsAuthenticationChallenge(response, isProxyAuth))
 0207                            {
 208                                // Tail response for Negotiate on successful authentication. Validate it before we proce
 0209                                authContext.GetOutgoingBlob(challengeData, out statusCode);
 0210                                if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded)
 0211                                {
 0212                                    isNewConnection = false;
 0213                                    connection.Dispose();
 0214                                    throw new HttpRequestException(HttpRequestError.UserAuthenticationError, SR.Format(S
 215                                }
 0216                                break;
 217                            }
 218
 0219                            needDrain = true;
 0220                        }
 0221                    }
 222                    finally
 0223                    {
 0224                        if (isNewConnection)
 0225                        {
 0226                            connection!.Release();
 0227                        }
 0228                    }
 0229                }
 0230            }
 231
 0232            return response!;
 0233        }
 234
 235        public static Task<HttpResponseMessage> SendWithNtProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bool 
 0236        {
 0237            return SendWithNtAuthAsync(request, proxyUri, async, proxyCredentials, impersonationLevel, isProxyAuth: true
 0238        }
 239
 240        public static Task<HttpResponseMessage> SendWithNtConnectionAuthAsync(HttpRequestMessage request, bool async, IC
 0241        {
 0242            Debug.Assert(request.RequestUri != null);
 0243            return SendWithNtAuthAsync(request, request.RequestUri, async, credentials, impersonationLevel, isProxyAuth:
 0244        }
 245    }
 246}

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()
InnerSendAsync(System.Net.Http.HttpRequestMessage,System.Boolean,System.Boolean,System.Boolean,System.Net.Http.HttpConnectionPool,System.Threading.CancellationToken)
SendWithAuthAsync()
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()
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)
.cctor()
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()
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)