| | | 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 | | |
| | | 4 | | using System.Diagnostics; |
| | | 5 | | using System.Diagnostics.CodeAnalysis; |
| | | 6 | | using System.Net.Http.Headers; |
| | | 7 | | using System.Text; |
| | | 8 | | using System.Threading; |
| | | 9 | | using System.Threading.Tasks; |
| | | 10 | | |
| | | 11 | | namespace 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 | | { |
| | 0 | 30 | | public AuthenticationType AuthenticationType { get; } |
| | 0 | 31 | | public string SchemeName { get; } |
| | 0 | 32 | | public NetworkCredential Credential { get; } |
| | 0 | 33 | | public string? ChallengeData { get; } |
| | | 34 | | |
| | | 35 | | public AuthenticationChallenge(AuthenticationType authenticationType, string schemeName, NetworkCredential c |
| | 0 | 36 | | { |
| | 0 | 37 | | AuthenticationType = authenticationType; |
| | 0 | 38 | | SchemeName = schemeName; |
| | 0 | 39 | | Credential = credential; |
| | 0 | 40 | | ChallengeData = challenge; |
| | 0 | 41 | | } |
| | | 42 | | } |
| | | 43 | | |
| | | 44 | | private static bool TryGetChallengeDataForScheme(string scheme, HttpHeaderValueCollection<AuthenticationHeaderVa |
| | 0 | 45 | | { |
| | 0 | 46 | | foreach (AuthenticationHeaderValue ahv in authenticationHeaderValues) |
| | 0 | 47 | | { |
| | 0 | 48 | | if (StringComparer.OrdinalIgnoreCase.Equals(scheme, ahv.Scheme)) |
| | 0 | 49 | | { |
| | | 50 | | // Note, a valid challenge can have challengeData == null |
| | 0 | 51 | | challengeData = ahv.Parameter; |
| | 0 | 52 | | return true; |
| | | 53 | | } |
| | 0 | 54 | | } |
| | | 55 | | |
| | 0 | 56 | | challengeData = null; |
| | 0 | 57 | | return false; |
| | 0 | 58 | | } |
| | | 59 | | |
| | | 60 | | // Helper function to determine if response is part of session-based authentication challenge. |
| | | 61 | | internal static bool IsSessionAuthenticationChallenge(HttpResponseMessage response) |
| | 0 | 62 | | { |
| | 0 | 63 | | if (response.StatusCode != HttpStatusCode.Unauthorized) |
| | 0 | 64 | | { |
| | 0 | 65 | | return false; |
| | | 66 | | } |
| | | 67 | | |
| | 0 | 68 | | HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues = GetResponseAuthenticationH |
| | 0 | 69 | | foreach (AuthenticationHeaderValue ahv in authenticationHeaderValues) |
| | 0 | 70 | | { |
| | 0 | 71 | | if (StringComparer.OrdinalIgnoreCase.Equals(NegotiateScheme, ahv.Scheme) || StringComparer.OrdinalIgnore |
| | 0 | 72 | | { |
| | 0 | 73 | | return true; |
| | | 74 | | } |
| | 0 | 75 | | } |
| | | 76 | | |
| | 0 | 77 | | return false; |
| | 0 | 78 | | } |
| | | 79 | | |
| | | 80 | | private static bool TryGetValidAuthenticationChallengeForScheme(string scheme, AuthenticationType authentication |
| | | 81 | | HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues, out AuthenticationChallenge |
| | 0 | 82 | | { |
| | 0 | 83 | | challenge = default; |
| | | 84 | | |
| | 0 | 85 | | if (!TryGetChallengeDataForScheme(scheme, authenticationHeaderValues, out string? challengeData)) |
| | 0 | 86 | | { |
| | 0 | 87 | | return false; |
| | | 88 | | } |
| | | 89 | | |
| | 0 | 90 | | NetworkCredential? credential = credentials.GetCredential(uri, scheme); |
| | 0 | 91 | | if (credential == null) |
| | 0 | 92 | | { |
| | | 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. |
| | 0 | 95 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 96 | | { |
| | 0 | 97 | | NetEventSource.AuthenticationInfo(uri, $"Authentication scheme '{scheme}' supported by server, but n |
| | 0 | 98 | | } |
| | 0 | 99 | | return false; |
| | | 100 | | } |
| | | 101 | | |
| | 0 | 102 | | challenge = new AuthenticationChallenge(authenticationType, scheme, credential, challengeData); |
| | 0 | 103 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 104 | | { |
| | 0 | 105 | | NetEventSource.AuthenticationInfo(uri, $"Authentication scheme '{scheme}' selected. Client username={cha |
| | 0 | 106 | | } |
| | 0 | 107 | | return true; |
| | 0 | 108 | | } |
| | | 109 | | |
| | | 110 | | private static bool TryGetAuthenticationChallenge(HttpResponseMessage response, bool isProxyAuth, Uri authUri, I |
| | 0 | 111 | | { |
| | 0 | 112 | | if (!IsAuthenticationChallenge(response, isProxyAuth)) |
| | 0 | 113 | | { |
| | 0 | 114 | | challenge = default; |
| | 0 | 115 | | return false; |
| | | 116 | | } |
| | | 117 | | |
| | | 118 | | // Try to get a valid challenge for the schemes we support, in priority order. |
| | 0 | 119 | | HttpHeaderValueCollection<AuthenticationHeaderValue> authenticationHeaderValues = GetResponseAuthenticationH |
| | 0 | 120 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 121 | | { |
| | 0 | 122 | | NetEventSource.AuthenticationInfo(authUri, $"{(isProxyAuth ? "Proxy" : "Server")} authentication request |
| | 0 | 123 | | } |
| | 0 | 124 | | return |
| | 0 | 125 | | TryGetValidAuthenticationChallengeForScheme(NegotiateScheme, AuthenticationType.Negotiate, authUri, cred |
| | 0 | 126 | | TryGetValidAuthenticationChallengeForScheme(NtlmScheme, AuthenticationType.Ntlm, authUri, credentials, a |
| | 0 | 127 | | TryGetValidAuthenticationChallengeForScheme(DigestScheme, AuthenticationType.Digest, authUri, credential |
| | 0 | 128 | | TryGetValidAuthenticationChallengeForScheme(BasicScheme, AuthenticationType.Basic, authUri, credentials, |
| | 0 | 129 | | } |
| | | 130 | | |
| | | 131 | | private static bool TryGetRepeatedChallenge(HttpResponseMessage response, string scheme, bool isProxyAuth, out s |
| | 0 | 132 | | { |
| | 0 | 133 | | challengeData = null; |
| | | 134 | | |
| | 0 | 135 | | if (!IsAuthenticationChallenge(response, isProxyAuth)) |
| | 0 | 136 | | { |
| | 0 | 137 | | return false; |
| | | 138 | | } |
| | | 139 | | |
| | 0 | 140 | | if (!TryGetChallengeDataForScheme(scheme, GetResponseAuthenticationHeaderValues(response, isProxyAuth), out |
| | 0 | 141 | | { |
| | | 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. |
| | 0 | 144 | | return false; |
| | | 145 | | } |
| | | 146 | | |
| | 0 | 147 | | return true; |
| | 0 | 148 | | } |
| | | 149 | | |
| | | 150 | | private static bool IsAuthenticationChallenge(HttpResponseMessage response, bool isProxyAuth) |
| | 0 | 151 | | { |
| | 0 | 152 | | return isProxyAuth ? |
| | 0 | 153 | | response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired : |
| | 0 | 154 | | response.StatusCode == HttpStatusCode.Unauthorized; |
| | 0 | 155 | | } |
| | | 156 | | |
| | | 157 | | private static HttpHeaderValueCollection<AuthenticationHeaderValue> GetResponseAuthenticationHeaderValues(HttpRe |
| | 0 | 158 | | { |
| | 0 | 159 | | return isProxyAuth ? |
| | 0 | 160 | | response.Headers.ProxyAuthenticate : |
| | 0 | 161 | | response.Headers.WwwAuthenticate; |
| | 0 | 162 | | } |
| | | 163 | | |
| | | 164 | | private static void SetRequestAuthenticationHeaderValue(HttpRequestMessage request, AuthenticationHeaderValue he |
| | 0 | 165 | | { |
| | 0 | 166 | | if (isProxyAuth) |
| | 0 | 167 | | { |
| | 0 | 168 | | request.Headers.ProxyAuthorization = headerValue; |
| | 0 | 169 | | } |
| | | 170 | | else |
| | 0 | 171 | | { |
| | 0 | 172 | | request.Headers.Authorization = headerValue; |
| | 0 | 173 | | } |
| | 0 | 174 | | } |
| | | 175 | | |
| | | 176 | | private static void SetBasicAuthToken(HttpRequestMessage request, NetworkCredential credential, bool isProxyAuth |
| | 0 | 177 | | { |
| | 0 | 178 | | string authString = !string.IsNullOrEmpty(credential.Domain) ? |
| | 0 | 179 | | credential.Domain + "\\" + credential.UserName + ":" + credential.Password : |
| | 0 | 180 | | credential.UserName + ":" + credential.Password; |
| | | 181 | | |
| | 0 | 182 | | string base64AuthString = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString)); |
| | | 183 | | |
| | 0 | 184 | | SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(BasicScheme, base64AuthString), i |
| | 0 | 185 | | } |
| | | 186 | | |
| | | 187 | | private static async ValueTask<bool> TrySetDigestAuthToken(HttpRequestMessage request, NetworkCredential credent |
| | 0 | 188 | | { |
| | 0 | 189 | | 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 |
| | 0 | 192 | | if (string.IsNullOrEmpty(parameter)) |
| | 0 | 193 | | { |
| | 0 | 194 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 195 | | { |
| | 0 | 196 | | NetEventSource.AuthenticationError(request.RequestUri, $"Unable to find 'Digest' authentication toke |
| | 0 | 197 | | } |
| | 0 | 198 | | return false; |
| | | 199 | | } |
| | | 200 | | |
| | 0 | 201 | | var headerValue = new AuthenticationHeaderValue(DigestScheme, parameter); |
| | 0 | 202 | | SetRequestAuthenticationHeaderValue(request, headerValue, isProxyAuth); |
| | 0 | 203 | | return true; |
| | 0 | 204 | | } |
| | | 205 | | |
| | | 206 | | private static ValueTask<HttpResponseMessage> InnerSendAsync(HttpRequestMessage request, bool async, bool isProx |
| | 0 | 207 | | { |
| | 0 | 208 | | return isProxyAuth ? |
| | 0 | 209 | | pool.SendWithVersionDetectionAndRetryAsync(request, async, doRequestAuth, cancellationToken) : |
| | 0 | 210 | | pool.SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken); |
| | 0 | 211 | | } |
| | | 212 | | |
| | | 213 | | private static async ValueTask<HttpResponseMessage> SendWithAuthAsync(HttpRequestMessage request, Uri authUri, b |
| | 0 | 214 | | { |
| | | 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. |
| | 0 | 217 | | NetworkCredential? preAuthCredential = null; |
| | 0 | 218 | | Uri? preAuthCredentialUri = null; |
| | 0 | 219 | | if (preAuthenticate) |
| | 0 | 220 | | { |
| | 0 | 221 | | if (isProxyAuth) |
| | 0 | 222 | | { |
| | | 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. |
| | 0 | 226 | | NetworkCredential? credential = credentials.GetCredential(authUri, BasicScheme); |
| | 0 | 227 | | if (credential != null && credential != CredentialCache.DefaultNetworkCredentials) |
| | 0 | 228 | | { |
| | 0 | 229 | | preAuthCredential = credential; |
| | 0 | 230 | | SetBasicAuthToken(request, credential, isProxyAuth: true); |
| | 0 | 231 | | } |
| | 0 | 232 | | } |
| | | 233 | | else |
| | 0 | 234 | | { |
| | | 235 | | // For request pre-authentication, look up credentials from the preauth cache. |
| | 0 | 236 | | Debug.Assert(pool.PreAuthCredentials != null); |
| | | 237 | | (Uri uriPrefix, NetworkCredential credential)? preAuthCredentialPair; |
| | 0 | 238 | | lock (pool.PreAuthCredentials) |
| | 0 | 239 | | { |
| | | 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. |
| | 0 | 242 | | Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NegotiateScheme) == null); |
| | 0 | 243 | | Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NtlmScheme) == null); |
| | 0 | 244 | | Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, DigestScheme) == null); |
| | 0 | 245 | | preAuthCredentialPair = pool.PreAuthCredentials.GetCredential(authUri, BasicScheme); |
| | 0 | 246 | | } |
| | | 247 | | |
| | 0 | 248 | | if (preAuthCredentialPair != null) |
| | 0 | 249 | | { |
| | 0 | 250 | | (preAuthCredentialUri, preAuthCredential) = preAuthCredentialPair.Value; |
| | 0 | 251 | | SetBasicAuthToken(request, preAuthCredential, isProxyAuth); |
| | 0 | 252 | | } |
| | 0 | 253 | | } |
| | 0 | 254 | | } |
| | | 255 | | |
| | 0 | 256 | | HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancel |
| | | 257 | | |
| | 0 | 258 | | if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge c |
| | 0 | 259 | | { |
| | 0 | 260 | | switch (challenge.AuthenticationType) |
| | | 261 | | { |
| | | 262 | | case AuthenticationType.Digest: |
| | 0 | 263 | | if (CredentialCache.DefaultCredentials == credentials) |
| | 0 | 264 | | { |
| | | 265 | | // The DefaultCredentials applies only to NTLM, negotiate, and Kerberos-based authentication |
| | 0 | 266 | | break; |
| | | 267 | | } |
| | | 268 | | |
| | 0 | 269 | | var digestResponse = new DigestResponse(challenge.ChallengeData); |
| | 0 | 270 | | if (await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAuth).Conf |
| | 0 | 271 | | { |
| | 0 | 272 | | response.Dispose(); |
| | 0 | 273 | | response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellati |
| | | 274 | | |
| | | 275 | | // Retry in case of nonce timeout in server. |
| | 0 | 276 | | if (TryGetRepeatedChallenge(response, challenge.SchemeName, isProxyAuth, out string? challen |
| | 0 | 277 | | { |
| | 0 | 278 | | digestResponse = new DigestResponse(challengeData); |
| | 0 | 279 | | if (IsServerNonceStale(digestResponse) && |
| | 0 | 280 | | await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isProxyAu |
| | 0 | 281 | | { |
| | 0 | 282 | | response.Dispose(); |
| | 0 | 283 | | response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, ca |
| | 0 | 284 | | } |
| | 0 | 285 | | } |
| | 0 | 286 | | } |
| | 0 | 287 | | break; |
| | | 288 | | |
| | | 289 | | case AuthenticationType.Basic: |
| | 0 | 290 | | if (CredentialCache.DefaultCredentials == credentials) |
| | 0 | 291 | | { |
| | | 292 | | // The DefaultCredentials applies only to NTLM, negotiate, and Kerberos-based authentication |
| | 0 | 293 | | break; |
| | | 294 | | } |
| | | 295 | | |
| | 0 | 296 | | if (preAuthCredential != null) |
| | 0 | 297 | | { |
| | 0 | 298 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 299 | | { |
| | 0 | 300 | | NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyAuth ? "p |
| | 0 | 301 | | } |
| | | 302 | | |
| | 0 | 303 | | if (challenge.Credential == preAuthCredential) |
| | 0 | 304 | | { |
| | | 305 | | // Pre auth failed, and user supplied credentials are still same, we can stop there. |
| | 0 | 306 | | break; |
| | | 307 | | } |
| | | 308 | | |
| | | 309 | | // Pre-auth credentials have changed, continue with the new ones. |
| | | 310 | | // The old ones will be removed below. |
| | 0 | 311 | | } |
| | | 312 | | |
| | 0 | 313 | | response.Dispose(); |
| | 0 | 314 | | SetBasicAuthToken(request, challenge.Credential, isProxyAuth); |
| | 0 | 315 | | response = await InnerSendAsync(request, async, isProxyAuth, doRequestAuth, pool, cancellationTo |
| | | 316 | | |
| | 0 | 317 | | if (preAuthenticate && !isProxyAuth) |
| | 0 | 318 | | { |
| | 0 | 319 | | switch (response.StatusCode) |
| | | 320 | | { |
| | | 321 | | case HttpStatusCode.ProxyAuthenticationRequired: |
| | | 322 | | case HttpStatusCode.Unauthorized: |
| | 0 | 323 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 324 | | { |
| | 0 | 325 | | NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyA |
| | 0 | 326 | | } |
| | 0 | 327 | | break; |
| | | 328 | | |
| | | 329 | | default: |
| | 0 | 330 | | lock (pool.PreAuthCredentials!) |
| | 0 | 331 | | { |
| | | 332 | | // remove previously cached (failing) creds |
| | 0 | 333 | | if (preAuthCredentialUri != null) |
| | 0 | 334 | | { |
| | 0 | 335 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 336 | | { |
| | 0 | 337 | | NetEventSource.Info(pool.PreAuthCredentials, $"Removing Basic credential |
| | 0 | 338 | | } |
| | | 339 | | |
| | 0 | 340 | | pool.PreAuthCredentials.Remove(preAuthCredentialUri, BasicScheme); |
| | 0 | 341 | | } |
| | | 342 | | |
| | | 343 | | try |
| | 0 | 344 | | { |
| | 0 | 345 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 346 | | { |
| | 0 | 347 | | NetEventSource.Info(pool.PreAuthCredentials, $"Adding Basic credential t |
| | 0 | 348 | | } |
| | 0 | 349 | | pool.PreAuthCredentials.Add(authUri, BasicScheme, challenge.Credential); |
| | 0 | 350 | | } |
| | 0 | 351 | | catch (ArgumentException) |
| | 0 | 352 | | { |
| | | 353 | | // The credential already existed. |
| | 0 | 354 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 355 | | { |
| | 0 | 356 | | NetEventSource.Info(pool.PreAuthCredentials, $"Basic credential present |
| | 0 | 357 | | } |
| | 0 | 358 | | } |
| | 0 | 359 | | } |
| | 0 | 360 | | break; |
| | | 361 | | } |
| | 0 | 362 | | } |
| | 0 | 363 | | break; |
| | | 364 | | } |
| | 0 | 365 | | } |
| | | 366 | | |
| | 0 | 367 | | if (NetEventSource.Log.IsEnabled() && response.StatusCode == HttpStatusCode.Unauthorized) |
| | 0 | 368 | | { |
| | 0 | 369 | | NetEventSource.AuthenticationError(authUri, $"{(isProxyAuth ? "Proxy" : "Server")} authentication failed |
| | 0 | 370 | | } |
| | | 371 | | |
| | 0 | 372 | | return response; |
| | 0 | 373 | | } |
| | | 374 | | |
| | | 375 | | public static ValueTask<HttpResponseMessage> SendWithProxyAuthAsync(HttpRequestMessage request, Uri proxyUri, bo |
| | 0 | 376 | | { |
| | 0 | 377 | | return SendWithAuthAsync(request, proxyUri, async, proxyCredentials, preAuthenticate: GlobalHttpSettings.Soc |
| | 0 | 378 | | } |
| | | 379 | | |
| | | 380 | | public static ValueTask<HttpResponseMessage> SendWithRequestAuthAsync(HttpRequestMessage request, bool async, IC |
| | 0 | 381 | | { |
| | 0 | 382 | | Debug.Assert(request.RequestUri != null); |
| | 0 | 383 | | return SendWithAuthAsync(request, request.RequestUri, async, credentials, preAuthenticate, isProxyAuth: fals |
| | 0 | 384 | | } |
| | | 385 | | } |
| | | 386 | | } |
| | | 387 | | |