Skip to content

Commit 3fac575

Browse files
authored
Refresh cached credentials after PreAuthenticate fails (#101053)
* Support refreshing credentials in pre-auth cache * Fix minor bug in CredentialCache * Add unit test * Fix tests * Fix tests attempt 2 * Merge two lock statements. * Fix build
1 parent d7a6ffa commit 3fac575

File tree

10 files changed

+363
-130
lines changed

10 files changed

+363
-130
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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.Collections;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.Globalization;
9+
10+
namespace System.Net
11+
{
12+
internal sealed class CredentialCacheKey : IEquatable<CredentialCacheKey?>
13+
{
14+
public readonly Uri UriPrefix;
15+
public readonly int UriPrefixLength = -1;
16+
public readonly string AuthenticationType;
17+
18+
internal CredentialCacheKey(Uri uriPrefix, string authenticationType)
19+
{
20+
Debug.Assert(uriPrefix != null);
21+
Debug.Assert(authenticationType != null);
22+
23+
UriPrefix = uriPrefix;
24+
UriPrefixLength = UriPrefix.AbsolutePath.LastIndexOf('/');
25+
AuthenticationType = authenticationType;
26+
}
27+
28+
internal bool Match(Uri uri, string authenticationType)
29+
{
30+
if (uri == null || authenticationType == null)
31+
{
32+
return false;
33+
}
34+
35+
// If the protocols don't match, this credential is not applicable for the given Uri.
36+
if (!string.Equals(authenticationType, AuthenticationType, StringComparison.OrdinalIgnoreCase))
37+
{
38+
return false;
39+
}
40+
41+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Match({UriPrefix} & {uri})");
42+
43+
return IsPrefix(uri, UriPrefix);
44+
}
45+
46+
// IsPrefix (Uri)
47+
//
48+
// Determines whether <prefixUri> is a prefix of this URI. A prefix
49+
// match is defined as:
50+
//
51+
// scheme match
52+
// + host match
53+
// + port match, if any
54+
// + <prefix> path is a prefix of <URI> path, if any
55+
//
56+
// Returns:
57+
// True if <prefixUri> is a prefix of this URI
58+
private static bool IsPrefix(Uri uri, Uri prefixUri)
59+
{
60+
Debug.Assert(uri != null);
61+
Debug.Assert(prefixUri != null);
62+
63+
if (prefixUri.Scheme != uri.Scheme || prefixUri.Host != uri.Host || prefixUri.Port != uri.Port)
64+
{
65+
return false;
66+
}
67+
68+
int prefixLen = prefixUri.AbsolutePath.LastIndexOf('/');
69+
if (prefixLen > uri.AbsolutePath.LastIndexOf('/'))
70+
{
71+
return false;
72+
}
73+
74+
return string.Compare(uri.AbsolutePath, 0, prefixUri.AbsolutePath, 0, prefixLen, StringComparison.OrdinalIgnoreCase) == 0;
75+
}
76+
77+
public override int GetHashCode() =>
78+
StringComparer.OrdinalIgnoreCase.GetHashCode(AuthenticationType) ^
79+
UriPrefix.GetHashCode();
80+
81+
public bool Equals([NotNullWhen(true)] CredentialCacheKey? other)
82+
{
83+
if (other == null)
84+
{
85+
return false;
86+
}
87+
88+
bool equals =
89+
string.Equals(AuthenticationType, other.AuthenticationType, StringComparison.OrdinalIgnoreCase) &&
90+
UriPrefix.Equals(other.UriPrefix);
91+
92+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Equals({this},{other}) returns {equals}");
93+
94+
return equals;
95+
}
96+
97+
public override bool Equals([NotNullWhen(true)] object? obj) => Equals(obj as CredentialCacheKey);
98+
99+
public override string ToString() =>
100+
string.Create(CultureInfo.InvariantCulture, $"[{UriPrefixLength}]:{UriPrefix}:{AuthenticationType}");
101+
}
102+
103+
internal static class CredentialCacheHelper
104+
{
105+
public static bool TryGetCredential(Dictionary<CredentialCacheKey, NetworkCredential> cache, Uri uriPrefix, string authType, [NotNullWhen(true)] out Uri? mostSpecificMatchUri, [NotNullWhen(true)] out NetworkCredential? mostSpecificMatch)
106+
{
107+
int longestMatchPrefix = -1;
108+
mostSpecificMatch = null;
109+
mostSpecificMatchUri = null;
110+
111+
// Enumerate through every credential in the cache
112+
foreach ((CredentialCacheKey key, NetworkCredential value) in cache)
113+
{
114+
// Determine if this credential is applicable to the current Uri/AuthType
115+
if (key.Match(uriPrefix, authType))
116+
{
117+
int prefixLen = key.UriPrefixLength;
118+
119+
// Check if the match is better than the current-most-specific match
120+
if (prefixLen > longestMatchPrefix)
121+
{
122+
// Yes: update the information about currently preferred match
123+
longestMatchPrefix = prefixLen;
124+
mostSpecificMatch = value;
125+
mostSpecificMatchUri = key.UriPrefix;
126+
}
127+
}
128+
}
129+
130+
return mostSpecificMatch != null;
131+
}
132+
}
133+
}

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@
164164
Link="Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs" />
165165
<Compile Include="$(CommonPath)System\Obsoletions.cs"
166166
Link="Common\System\Obsoletions.cs" />
167+
<Compile Include="$(CommonPath)System\Net\CredentialCacheKey.cs"
168+
Link="Common\System\Net\CredentialCacheKey.cs" />
167169
</ItemGroup>
168170

169171
<!-- SocketsHttpHandler implementation -->
@@ -216,6 +218,7 @@
216218
<Compile Include="System\Net\Http\SocketsHttpHandler\IHttpTrace.cs" />
217219
<Compile Include="System\Net\Http\SocketsHttpHandler\IMultiWebProxy.cs" />
218220
<Compile Include="System\Net\Http\SocketsHttpHandler\MultiProxy.cs" />
221+
<Compile Include="System\Net\Http\SocketsHttpHandler\PreAuthCredentialCache.cs" />
219222
<Compile Include="System\Net\Http\SocketsHttpHandler\RawConnectionStream.cs" />
220223
<Compile Include="System\Net\Http\SocketsHttpHandler\RedirectHandler.cs" />
221224
<Compile Include="System\Net\Http\SocketsHttpHandler\SocketsHttpConnectionContext.cs" />

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -215,25 +215,26 @@ private static async ValueTask<HttpResponseMessage> SendWithAuthAsync(HttpReques
215215
// If preauth is enabled and this isn't proxy auth, try to get a basic credential from the
216216
// preauth credentials cache, and if successful, set an auth header for it onto the request.
217217
// Currently we only support preauth for Basic.
218-
bool performedBasicPreauth = false;
218+
NetworkCredential? preAuthCredential = null;
219+
Uri? preAuthCredentialUri = null;
219220
if (preAuthenticate)
220221
{
221222
Debug.Assert(pool.PreAuthCredentials != null);
222-
NetworkCredential? credential;
223+
(Uri uriPrefix, NetworkCredential credential)? preAuthCredentialPair;
223224
lock (pool.PreAuthCredentials)
224225
{
225226
// Just look for basic credentials. If in the future we support preauth
226227
// for other schemes, this will need to search in order of precedence.
227228
Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NegotiateScheme) == null);
228229
Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NtlmScheme) == null);
229230
Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, DigestScheme) == null);
230-
credential = pool.PreAuthCredentials.GetCredential(authUri, BasicScheme);
231+
preAuthCredentialPair = pool.PreAuthCredentials.GetCredential(authUri, BasicScheme);
231232
}
232233

233-
if (credential != null)
234+
if (preAuthCredentialPair != null)
234235
{
235-
SetBasicAuthToken(request, credential, isProxyAuth);
236-
performedBasicPreauth = true;
236+
(preAuthCredentialUri, preAuthCredential) = preAuthCredentialPair.Value;
237+
SetBasicAuthToken(request, preAuthCredential, isProxyAuth);
237238
}
238239
}
239240

@@ -265,13 +266,21 @@ await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isPro
265266
break;
266267

267268
case AuthenticationType.Basic:
268-
if (performedBasicPreauth)
269+
if (preAuthCredential != null)
269270
{
270271
if (NetEventSource.Log.IsEnabled())
271272
{
272273
NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyAuth ? "proxy" : "server")} failed.");
273274
}
274-
break;
275+
276+
if (challenge.Credential == preAuthCredential)
277+
{
278+
// Pre auth failed, and user supplied credentials are still same, we can stop there.
279+
break;
280+
}
281+
282+
// Pre-auth credentials have changed, continue with the new ones.
283+
// The old ones will be removed below.
275284
}
276285

277286
response.Dispose();
@@ -293,6 +302,17 @@ await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isPro
293302
default:
294303
lock (pool.PreAuthCredentials!)
295304
{
305+
// remove previously cached (failing) creds
306+
if (preAuthCredentialUri != null)
307+
{
308+
if (NetEventSource.Log.IsEnabled())
309+
{
310+
NetEventSource.Info(pool.PreAuthCredentials, $"Removing Basic credential from cache, uri={preAuthCredentialUri}, username={preAuthCredential!.UserName}");
311+
}
312+
313+
pool.PreAuthCredentials.Remove(preAuthCredentialUri, BasicScheme);
314+
}
315+
296316
try
297317
{
298318
if (NetEventSource.Log.IsEnabled())

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ internal sealed partial class HttpConnectionPool : IDisposable
5959
private SslClientAuthenticationOptions? _sslOptionsHttp3;
6060
private readonly SslClientAuthenticationOptions? _sslOptionsProxy;
6161

62-
private readonly CredentialCache? _preAuthCredentials;
62+
private readonly PreAuthCredentialCache? _preAuthCredentials;
6363

6464
/// <summary>Whether the pool has been used since the last time a cleanup occurred.</summary>
6565
private bool _usedSinceLastCleanup = true;
@@ -237,7 +237,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK
237237
// Set up for PreAuthenticate. Access to this cache is guarded by a lock on the cache itself.
238238
if (_poolManager.Settings._preAuthenticate)
239239
{
240-
_preAuthCredentials = new CredentialCache();
240+
_preAuthCredentials = new PreAuthCredentialCache();
241241
}
242242

243243
_http11RequestQueue = new RequestQueue<HttpConnection>();
@@ -296,7 +296,7 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection
296296
public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.SslSocksTunnel;
297297
public Uri? ProxyUri => _proxyUri;
298298
public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials;
299-
public CredentialCache? PreAuthCredentials => _preAuthCredentials;
299+
public PreAuthCredentialCache? PreAuthCredentials => _preAuthCredentials;
300300
public bool IsDefaultPort => OriginAuthority.Port == (IsSecure ? DefaultHttpsPort : DefaultHttpPort);
301301
private bool DoProxyAuth => (_kind == HttpConnectionKind.Proxy || _kind == HttpConnectionKind.ProxyConnect);
302302

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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.Collections;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.Globalization;
9+
10+
namespace System.Net.Http
11+
{
12+
internal sealed class PreAuthCredentialCache
13+
{
14+
private Dictionary<CredentialCacheKey, NetworkCredential>? _cache;
15+
16+
public void Add(Uri uriPrefix, string authType, NetworkCredential cred)
17+
{
18+
Debug.Assert(uriPrefix != null);
19+
Debug.Assert(authType != null);
20+
21+
var key = new CredentialCacheKey(uriPrefix, authType);
22+
23+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Adding key:[{key}], cred:[{cred.Domain}],[{cred.UserName}]");
24+
25+
_cache ??= new Dictionary<CredentialCacheKey, NetworkCredential>();
26+
_cache.Add(key, cred);
27+
}
28+
29+
public void Remove(Uri uriPrefix, string authType)
30+
{
31+
Debug.Assert(uriPrefix != null);
32+
Debug.Assert(authType != null);
33+
34+
if (_cache == null)
35+
{
36+
return;
37+
}
38+
39+
var key = new CredentialCacheKey(uriPrefix, authType);
40+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Removing key:[{key}]");
41+
_cache.Remove(key);
42+
}
43+
44+
public (Uri uriPrefix, NetworkCredential credential)? GetCredential(Uri uriPrefix, string authType)
45+
{
46+
Debug.Assert(uriPrefix != null);
47+
Debug.Assert(authType != null);
48+
49+
if (_cache == null)
50+
{
51+
return null;
52+
}
53+
54+
CredentialCacheHelper.TryGetCredential(_cache, uriPrefix, authType, out Uri? mostSpecificMatchUri, out NetworkCredential? mostSpecificMatch);
55+
56+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Returning {(mostSpecificMatch == null ? "null" : "(" + mostSpecificMatch.UserName + ":" + mostSpecificMatch.Domain + ")")}");
57+
58+
return mostSpecificMatch == null ? null : (mostSpecificMatchUri!, mostSpecificMatch!);
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)