Repository navigation
Description
Description
When executing the provided code snippet in a .NET 8 project, an HttpRequestException is thrown with the message Authentication validation failed with error - GenericFailure. This issue occurs when attempting to send an HTTP request using HttpClient with Kerberos authentication and utilizing default credentials.
Reproduction Steps
- Create a new .NET 8 project.
- Add the following code to the Program.cs file:
using System.Net;
class Program
{
static async Task Main(string[] args)
{
var handler = new HttpClientHandler
{
UseDefaultCredentials = true,
};
using (var client = new HttpClient(handler))
{
try
{
HttpResponseMessage response = await client.GetAsync("...");
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}- Run the project.
Expected behavior
The HTTP request is successfully sent, and the response is processed without any exceptions.
Actual behavior
System.Net.Http.HttpRequestException: Authentication validation failed with error - GenericFailure.
at System.Net.Http.AuthenticationHelper.SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.AuthenticationHelper.SendWithAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean preAuthenticate, Boolean isProxyAuth, Boolean doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at Program.Main(String[] args) in C:\...\Program.cs:line 16
Regression?
In .NET 6 this issue does not occur.
Known Workarounds
- Changing the HTTP version to HTTP/1.0 by setting client.DefaultRequestVersion = HttpVersion.Version10.
- Using .NET 6 where this issue does not occur.
Configuration
-
.NET SDK Version: .NET8
-
Operating System: Win10
-
Server: Using gokrb5 Kerberos library
Other information
The problem may be related to the following code added in .NET 8:
Line 208 in eb765b7
| // Tail response for Negotiate on successful authentication. Validate it before we proceed. |
Additionally, in the gokrb5 library, there is a constant used for the WWW-Authenticate header set to the client upon successful authentication with HTTP code 200 . Could this be related to the issue?
Activity
ManickaP commentedon Jul 29, 2024
@wfurt any thoughts / suggestions?
wfurt commentedon Jul 29, 2024
This is going to be difficult to investigate IMHO without repro. The interesting part for me is the 1.0 vs 1.1 as workaround. That almost suggests the problem lives in session bases authentication somehow. (that may perhaps be verified with
Connection: closeheader)I would suggest to collect packet capture and internal traces and look for differences to start with.
korovindenis commentedon Jul 29, 2024
The issue is reproducible even with HTTP version 1.0 when the client includes the header Connection: Keep-Alive. Transitioning to this HTTP version does not seem to be a good solution.
In HTTP version 1.1, setting the header Connection: close does not resolve the issue.
As a workaround, modifying the constant to ade0234568a4209af8bc0280289eca has been effective. This value is derived from RFC 4559.
filipnavara commentedon Jul 31, 2024
Would it be possible for you to capture the connection trace with something like Fiddler or Proxyman and share it? I'd like to look at the exact authentication headers.
korovindenis commentedon Jul 31, 2024
@filipnavara
fiddler logs:
client
server
client
server
But there is an error in the net8 application.
filipnavara commentedon Jul 31, 2024
I'd need the request with the
Authorizationheader sent by the client, not just theWww-Authenticateresponses. Thanks!korovindenis commentedon Jul 31, 2024
@filipnavara I have added the logs (in the comment above), please let me know if any additional logs are needed.
filipnavara commentedon Jul 31, 2024
xxxxxxxx==doesn't sound like the real value. I understand if you want to keep the token private. I don't need the actual token value anyway.You can take the value a paste it into https://asn1js.eu/. Then copy the whole subtree by clicking on the root node and choosing
Copy subtreeand paste that here.Feel free to redact the
OCTET STRINGpart which contains the actual authentication token. I am only interested in the rest of the structure and the OIDs.For example, it may look like this (taken from example in MS specification):
The
OCTET_STRINGline is the exchanged token which I don't need. I need to see the rest of the metadata to figure out which specific protocol is the client trying to negotiate.korovindenis commentedon Jul 31, 2024
@filipnavara
filipnavara commentedon Jul 31, 2024
Thanks, this is helpful. It means the client is opportunistically sending the Kerberos token with OID
1.2.840.113554.1.2.2.That does match the OID in the server's response:
I'll need to dive into the (newer) SPNEGO specification to see what are the requirements for MIC (Message Integrity Check). At least the basic construction of the requests and responses look okay.
wfurt commentedon Jul 31, 2024
Since there seems to be two server constants - one working and one not - we can perhaps look at the difference...?
filipnavara commentedon Jul 31, 2024
The other one is an unparsable garbage, so not really much to compare :-/
jcmturner/gokrb5#549 (comment)
filipnavara commentedon Jul 31, 2024
I read the specification (RFC 4178) and the MIC is actually REQUIRED in this case. The rationale is the following:
1.2.840.48018.1.2.2despite the opportunistic token of type1.2.840.113554.1.2.2.1.2.840.113554.1.2.2is listed as the second most preferred mechanism.accept-completeand chosen mechanism1.2.840.113554.1.2.2.mechlistMICis OPTIONAL only if the accepted mechanism is the most preferred mechanism of both the initiator and the acceptor. That's not the case here, somechlistMICis REQUIRED.The server is not implementing the specification correctly. It needs to create the last response programmatically instead of using a predefined value, and it needs to compute the message integrity of the client's request and include it in the response.
AraHaan commentedon Jul 31, 2024
Kind of a strange issue though.
wfurt commentedon Aug 1, 2024
why? .NET 8 changed to improve security and verify the messages integrity. I should not be constant to start with.
ostrink commentedon Mar 20, 2025
For what it's worth, I had this problem porting a net472 application to net8. The exact same code worked correctly under net472 but returned 401 under net8.
After much investigation, it appears that net472 has some subtle redirection behavior. My URI was specified as "http://" but was automatically redirected to "https://".
The solution for me was to change my code to explicitly use "https://" under net8 and now the authentication works as expected.