-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HttpClientHandler with PreAuthenticate enabled can't refresh expired password #93340
Comments
Tagging subscribers to this area: @dotnet/ncl Issue DetailsDescriptionI have a client app that talks to remote HTTP servers via Say the customer logs into the website and changes their password, or we're sending OAuth2 tokens over Basic auth, rather than Bearer. It appears that FWIW, it also looks like there's a potential perf improvement by avoiding getting the credential twice. In the sample output below, look for the "Credential Plugin: returning credential" lines. Reproduction StepsHere's a sample app that demonstrates: using System.Net;
using System.Text;
HttpListener listener = new HttpListener();
string prefix = "http://localhost:1234/";
listener.Prefixes.Add(prefix);
listener.Start();
var credentialPlugin = new CredentialPlugin();
Task httpHandlerTask = HandleHttpRequests(listener);
using HttpClientHandler httpClientHandler = new()
{
PreAuthenticate = true,
Credentials = credentialPlugin
};
using HttpClient httpClient = new(httpClientHandler);
await SendRequest(httpClient, prefix);
await SendRequest(httpClient, prefix + "api1/ABC");
await SendRequest(httpClient, prefix + "api2/123");
credentialPlugin.RefreshToken();
await SendRequest(httpClient, prefix + "api1/DEF");
// Try another time, just in case the cred cache is updated after the first failed attempt
await SendRequest(httpClient, prefix + "api1/DEF");
await SendRequest(httpClient, prefix + "api2/456");
listener.Stop();
await httpHandlerTask;
async Task SendRequest(HttpClient client, string url)
{
using var response = await httpClient.GetAsync(url);
Console.WriteLine($"Client: response code {response.StatusCode} from {url}");
}
async Task HandleHttpRequests(HttpListener listener)
{
while (true)
{
try
{
var request = await listener.GetContextAsync();
var authenticationHeader = request.Request.Headers.Get("Authorization");
if (string.IsNullOrEmpty(authenticationHeader))
{
Console.WriteLine("Server: unauthenticated request to " + request.Request.RawUrl);
request.Response.AddHeader("WWW-Authenticate", "basic");
request.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
var auth = request.Request.Headers["Authorization"];
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(auth.Replace("Basic", "", StringComparison.OrdinalIgnoreCase)));
if (decoded == $"{credentialPlugin.UserName}:{credentialPlugin.Password}")
{
Console.WriteLine($"Server: authenticated request ({decoded}) to {request.Request.RawUrl}");
request.Response.StatusCode = (int)HttpStatusCode.OK;
}
else
{
Console.WriteLine($"Server: wrong username/password ({decoded}) request to " + request.Request.RawUrl);
request.Response.AddHeader("WWW-Authenticate", "basic");
request.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
}
request.Response.Close();
}
catch
{
// this is how http listener "gracefully" stops?
return;
}
}
}
class CredentialPlugin : ICredentials
{
public CredentialPlugin()
{
UserName = "username";
counter = 0;
Password = "password";
}
private int counter;
public string UserName {get; private set;}
public string Password { get;private set;}
// pretend this comes from an OAuth2 service
public void RefreshToken()
{
counter++;
Password = "password" + counter;
Console.WriteLine($"Credential Plugin: Changed to '{UserName}:{Password}'");
}
NetworkCredential? ICredentials.GetCredential(Uri uri, string authType)
{
Console.WriteLine($"Credential Plugin: returning credential '{UserName}:{Password}'");
return new NetworkCredential(UserName, Password);
}
} Expected behaviorAfter the "credential plugin" changes the password, I would expect HttpClient to:
Basically, when it gets an HTTP 401 response, I'd like it to clear the credentials from the pre-auth cache, and then mimic the behaviour when the first request didn't have auth in the first place. Here's pseudocode (that looks awfully like valid C# code) that explains my expected behaviour. Basically, try once (preauth if possible), and if the response is a 401, try a second time if credentials can be obtained. async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
HttpResponseMessage response;
if (_options.PreAuthenticate && TryGetCachedCredential(request.Uri, out var credential)
{
response = await SendWithAuthAsync(request, credential);
}
else
{
response = await SendVerbatimAsync(request);
}
if (response.Status == 401 && _options.Credentials != null)
{
credential = _options.Credentials.GetCredentials(request.Uri, response.Headers.WwwAuthorization.Scheme);
if (credential != null)
{
response = await SendWithAuthAsync(request, credential);
if (response.Status != 401)
{
UpdatePreauthCache(request.Uri, credential);
}
}
}
return response;
} I'm sure there's a lot more complexity than what I assume, but I hope it explains what I thought HttpClient was going to do, and what I'd like it to do. Actual behaviorHere's the output of my sample app. You can see it only tries the first password over and over, and despite asking the (simulated) "credential plugin" for a fresh token (twice per HTTP request), it keeps trying the old
Regression?No. Same behaviour on .NET Framework, and with WinHttpHandler. Known Workarounds
The problem with this is that it doubles the number of HTTP requests the client sends to the server (and increases server load) because time the app asks HttpClient to send a request, it's first sent unauthenticated, and then HttpClient will silently try again with credentials. Depending on the customer's network, the latency in doubling the number of requests can also have a noticeable perf impact, depending on the scenario.
In an app that supports NTLM, Kerberos/Negotiate, and Basic auth, the app has to increase complexity, duplicating some code that HttpClient already has to handle auth depending on the scheme requested by the 401 response's WWW-Authenticate header. ConfigurationNo response Other informationNo response
|
The behaviour is related to: runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs Lines 268 to 274 in f9cbb40
There are no comments to explain why it works in this way. A few lines earlier, on line 242, it obtains new credentials, but I don't understand why it bails out when |
From the docs: https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.preauthenticate?view=net-7.0
Granted, this sentence/paragraph is after another paragraph that starts "After a client request to a specific Uri is successfully authenticated". But English is a bit ambiguous, so I'm not sure the documentation is sufficiently clear to set expectations the behaviour is a bug (as I originally claimed), or not. The docs don't explicitly state what should be expected if the credentials change after HttpClientHandler caches credentials. I did notice that |
I have had a look at this, and the fix does not seem to be a simple oneliner as I originally thought. The underlying HTTP stack has a separate I see few options:
@dotnet/ncl, what do you think? |
Another, more drastic option would be not having the cache at all and always flowing the creds if
Neither the Basic auth RFC nor general HTTP Auth RFC prohibit clients sending auth headers without prior challenge from the server (the wording in general HTTP auth RFC actually suggests that servers should be prepared to receive unprompted Auth headers). |
I don't think we should go back to always |
Just as a technical possibility this could be solved by subclassing |
not sure I understand, the instance I am talking about is one internally created by the HTTP connection pool, and is solely under our control.
right, it might work but it really feels ugly |
I would probably support the idea of subclass (or private clone) if this is something the implementation keeps internally. One could implement We could also add public surface to |
We discussed this in the team and will go with something resembling the third option
Attempt will be to reuse as much as possible from the CredentialCache type to avoid reinventing the wheel. |
Description
I have a client app that talks to remote HTTP servers via
HttpClient
, and authentication handled viaHttpClientHandler.Credentials
, also usingHttpClientHandler.PreAuthenticate = true
to avoid needlessly hammering the web server with unauthenticated HTTP requests that need to get HTTP 401 responses (fewer requests also reduces latency, from the client point of view).Say the customer logs into the website and changes their password, or we're sending OAuth2 tokens over Basic auth, rather than Bearer. It appears that
HttpClientHandler
won't refresh the credential in its credential cache, despite getting a fresh credential fromHttpClientHandler.Credentials
.FWIW, it also looks like there's a potential perf improvement by avoiding getting the credential twice. In the sample output below, look for the "Credential Plugin: returning credential" lines.
Reproduction Steps
Here's a sample app that demonstrates:
Expected behavior
After the "credential plugin" changes the password, I would expect HttpClient to:
Basically, when it gets an HTTP 401 response, I'd like it to clear the credentials from the pre-auth cache, and then mimic the behaviour when the first request didn't have auth in the first place.
Here's pseudocode (that looks awfully like valid C# code) that explains my expected behaviour. Basically, try once (preauth if possible), and if the response is a 401, try a second time if credentials can be obtained.
I'm sure there's a lot more complexity than what I assume, but I hope it explains what I thought HttpClient was going to do, and what I'd like it to do.
Actual behavior
Here's the output of my sample app. You can see it only tries the first password over and over, and despite asking the (simulated) "credential plugin" for a fresh token (twice per HTTP request), it keeps trying the old
Regression?
No. Same behaviour on .NET Framework, and with WinHttpHandler.
Known Workarounds
The problem with this is that it doubles the number of HTTP requests the client sends to the server (and increases server load) because time the app asks HttpClient to send a request, it's first sent unauthenticated, and then HttpClient will silently try again with credentials. Depending on the customer's network, the latency in doubling the number of requests can also have a noticeable perf impact, depending on the scenario.
In an app that supports NTLM, Kerberos/Negotiate, and Basic auth, the app has to increase complexity, duplicating some code that HttpClient already has to handle auth depending on the scheme requested by the 401 response's WWW-Authenticate header.
Configuration
No response
Other information
No response
The text was updated successfully, but these errors were encountered: