Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting an NTLM Challenge from an AJAX POST on just one page

Quite the mystery here. I have an ASP.NET MVC 4 web application using Windows Authentication that has been maintained for over 18 months without issue. Recently, it was deployed to a fresh site and I've encountered the following, very strange behavior.

I am using a jQuery 1.8.2 $.ajax call to POST data to the server endpoints to update data. This works just fine except on one page, where the AJAX POST triggers a new NTLM Negotiation. The same problem is exhibited in Chrome, IE, and Firefox. While the issue is the same in all the browsers, it manifests itself in slightly different ways:

  • Firefox : Received a 401 Challenge response from the server and brings up a username/password dialog asking for credential in an infinite loop. Canceling the credential checks causes the request to fail with an Unauthorized response.
  • IE : No response from the server and the request status shows as "(Aborted)" in the Network monitor
  • Chrome : No response from the server and the request status shows a "(failure)" in the Network monitor.

The core issue seems to be that the Connection: keep-alive header is not being sent with the problematic AJAX request, but is in the other cases. However, the underlying JavaScript code is nearly identical, and the AJAX calls function properly in a development environment that is also set up to use Windows authentication.

Also, attempting the set the Connection request header in the beforeSend callback has no effect.

Any insights into the root of the problem, or ways to isolate whatever difference exists between the two AJAX POSTs are most appreciated.

Working Code and Request Headers

$.ajax({    url: url,    type: "POST",    data: $("#myForm").serialize(),    cache: false,    success: function (response) {    } });   Accept:*/* Accept-Encoding:gzip, deflate Accept-Language:en-US,en;q=0.8 Connection:keep-alive Content-Length:621 Content-Type:application/x-www-form-urlencoded; charset=UTF-8 Host:www.xxx.yyy.zzz Origin:http://www.xxx.yyy.zzz Referer:http://www.xxx.yyy.zzz/app/resource/path User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 X-Requested-With:XMLHttpRequest 

Failing Code and Request Headers

$.ajax({     url: url,     type: "POST",     data: data,     cache: false,     success: function (data, status, xhr) {     }  });   WARN: Provisional headers are shown  Accept:*/*  Content-Type:application/x-www-form-urlencoded; charset=UTF-8  Origin:http://www.xxx.yyy.zzz  Referer:http://www.xxx.yyy.zzz/app/resource/item/1  User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36  X-Requested-With:XMLHttpRequest 

I have also looked at the network process in the Chrome chrome://net-internals/#events viewer. Here is the event log from the failed request at the point it deviates from the successful one. Where the failed request gets an "HTTP/1.1 401 Unauthorized" the successful request gets an "HTTP/1.1 200 OK" response, presumably due to the presence of the Connection: keep-alive header.

2303: URL_REQUEST Start Time: 2015-04-28 13:53:41.788  t=14736 [st= 0] +REQUEST_ALIVE  [dt=71] t=14736 [st= 0]    URL_REQUEST_DELEGATE  [dt=0] t=14736 [st= 0]   +URL_REQUEST_START_JOB  [dt=70]                    --> load_flags = 2688000 (BYPASS_DATA_REDUCTION_PROXY | MAYBE_USER_GESTURE | REPORT_RAW_HEADERS | VERIFY_EV_CERT)                --> method = "POST"                --> priority = "LOW"                --> upload_id = "0"                --> url = "http://..." t=14736 [st= 0]      URL_REQUEST_DELEGATE  [dt=0] t=14736 [st= 0]      HTTP_CACHE_GET_BACKEND  [dt=0] t=14736 [st= 0]      URL_REQUEST_DELEGATE  [dt=0] t=14736 [st= 0]     +HTTP_STREAM_REQUEST  [dt=0] t=14736 [st= 0]        HTTP_STREAM_REQUEST_BOUND_TO_JOB                        --> source_dependency = 2305 (HTTP_STREAM_JOB) t=14736 [st= 0]     -HTTP_STREAM_REQUEST t=14736 [st= 0]     +HTTP_TRANSACTION_SEND_REQUEST  [dt=0] t=14736 [st= 0]        HTTP_TRANSACTION_SEND_REQUEST_HEADERS                        --> POST ... HTTP/1.1                        Host: www.xxx.yyy.zzz                        Connection: keep-alive                        Content-Length: 105                        Accept: */*                        Origin: http://www.xxx.yyy.zzz                        User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36                        X-Requested-With: XMLHttpRequest                        Content-Type: application/x-www-form-urlencoded; charset=UTF-8                        Referer: http://www.xxx.yyy.zzz/app/resource/item/1                        Accept-Encoding: gzip, deflate                        Accept-Language: en-US,en;q=0.8 t=14736 [st= 0]        HTTP_TRANSACTION_SEND_REQUEST_BODY                        --> did_merge = true                        --> is_chunked = false                        --> length = 105 t=14736 [st= 0]     -HTTP_TRANSACTION_SEND_REQUEST t=14736 [st= 0]     +HTTP_TRANSACTION_READ_HEADERS  [dt=0] t=14736 [st= 0]        HTTP_STREAM_PARSER_READ_HEADERS  [dt=0] t=14736 [st= 0]        HTTP_TRANSACTION_READ_RESPONSE_HEADERS                    --> HTTP/1.1 401 Unauthorized                        Content-Type: text/html                        Server: Microsoft-IIS/7.5                        WWW-Authenticate: Negotiate                        WWW-Authenticate: NTLM                        X-Powered-By: ASP.NET                        X-UA-Compatible: IE=9                        Date: Tue, 28 Apr 2015 18:53:41 GMT                        Content-Length: 1293 

Edit

Playing around with different request from the console gives the following table of results (under Chrome). The current base URL is http://IPAddress /app/topic/item and all test simply execute an $.ajax({ url: url, type: 'POST' })

+--------------------------------------+----------------------------+ |   URL                                | Response                   | +--------------------------------------+----------------------------+ | http://IP/app/topic/item/1/subitem/1 | net::ERR_INVALID_HANDLE    | | //IP/app/topic/item/1/subitem/1      | net::ERR_INVALID_HANDLE    | | /app/topic/item/1/subitem/1          | net::ERR_INVALID_HANDLE    | | 1/subitem/1                          | net::ERR_INVALID_HANDLE    | | 1/foo                                | 404 (Not Found) [expected] | | 1                                    | 302 (Redirect)  [expected] | +--------------------------------------+----------------------------+ 

Because the error only affects a subset of the POST action methods in one controller, I had initially thought this was a server-side issue, but after uncovering the issue of the missing Connection header, it actually appear to be a client-side issue. Exactly how the problem is triggered remains a mystery to me.

I did also verify that the Response headers for the working page and the problematic page are the same. Most relevant, the Persistent-Auth: true header is always returned in both cases.

like image 838
Lucas Avatar asked Apr 28 '15 19:04

Lucas


1 Answers

Some wild guesses:

  • This happens when you request a Role that is not in the Claims of the currently logged-in user. Verify that, if you're using [Authorize(Roles = "xyz")], the current user actually has that role.

  • It is not clear if your application uses a cookie for authentication. If yes, you should see it with the request. Are you setting withCredentials: true for each request?

like image 142
huysentruitw Avatar answered Sep 21 '22 18:09

huysentruitw