26

As from Chrome version 37, pre-flighted, cross-domain requests are failing (again) if the server has authentication enabled, even though all CORS headers are set correctly. This is on localhost (my dev PC).

Some of you may be aware of the history of Chrome/CORS/auth bugs, especially when HTTPS was involved. My problem does not involve HTTPS: I have an AngularJS application served from localhost:8383 talking to a Java (Jetty) server on localhost:8081 that has HTTP BASIC auth activated. GETs work fine, but POSTs fail with a 401:

XMLHttpRequest cannot load http://localhost:8081/cellnostics/rest/patient.
Invalid HTTP status code 401

I have previously written a custom (Java) CORS filter that sets the correct CORS headers, which worked up until v36. It fails in v37 and also the latest v38 (38.0.2125.101 m). It still works as expected with Internet Explorer 11 (11.0.9600) and Opera 12.17 (build 1863).

GET requests succeed, but POSTs fail. It looks like Chrome is pre-flighting all my POSTs due to the content-type: "application/json", and that it is the pre-flighted OPTIONS request that is failing.

In the Angular app I explicitly set the following request headers. AFAIK this setting for withCredentials should ensure that credentials are sent even for OPTIONS requests:

//Enable cross domain calls
$httpProvider.defaults.useXDomain = true;

//Send all requests, even OPTIONS, with credentials
$httpProvider.defaults.withCredentials = true;

Below is the request/response. You can see that the OPTIONS method is enabled in the Access-Control-Allow-Methods header. You can also see that the Javascript app's origin is explicitly enabled: Access-Control-Allow-Origin: http://localhost:8383.

Remote Address:[::1]:8081
Request URL:http://localhost:8081/cellnostics/rest/medicaltest
Request Method:OPTIONS
Status Code:401 Full authentication is required to access this resource

Request headers:

Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8,af;q=0.6
Access-Control-Request-Headers:accept, content-type
Access-Control-Request-Method:POST
Connection:keep-alive
Host:localhost:8081
Origin:http://localhost:8383
Referer:http://localhost:8383/celln-web/index.html
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36

Response headers:

Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With, Accept
Access-Control-Allow-Methods:POST, GET, OPTIONS, PUT, DELETE
Access-Control-Allow-Origin:http://localhost:8383
Access-Control-Max-Age:3600
Content-Length:0
Server:Jetty(8.1.8.v20121106)
WWW-Authenticate:Basic realm="Cellnostics"

Has anyone got any idea what else I should do? I made sure to clear the Chrome cache before testing, restarting and ensuring that there were no background Chrome processes left running before restart, so I'm pretty sure that there were no lingering auth cache issues.

I've had to switch to IE 11 for testing my web development. The fact that the same client and server setup still works for IE and Opera, and the fact that there is a history of Chrome/CORS bugs, makes me suspect Chrome.

EDIT: Here's an extract from the Chrome net-internals event list:

t=108514 [st=0]   +URL_REQUEST_START_JOB  [dt=4]
    --> load_flags = 336011264 (BYPASS_DATA_REDUCTION_PROXY | DO_NOT_SAVE_COOKIES | DO_NOT_SEND_AUTH_DATA | DO_NOT_SEND_COOKIES | MAYBE_USER_GESTURE | VERIFY_EV_CERT)
    --> method = "OPTIONS"
    --> priority = "LOW"
    --> url = "http://localhost:8081/cellnostics/rest/patient"
...
t=108516 [st=2] HTTP_TRANSACTION_SEND_REQUEST_HEADERS
--> OPTIONS /cellnostics/rest/patient HTTP/1.1
   Host: localhost:8081
   Connection: keep-alive
   Access-Control-Request-Method: POST
   Origin: http://localhost:8383
   User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.101 Safari/537.36
   Access-Control-Request-Headers: accept, content-type
   Accept: */*
   Referer: http://localhost:8383/celln-web/index.html
   Accept-Encoding: gzip,deflate,sdch
   Accept-Language: en-US,en;q=0.8,af;q=0.6

So it looks like the Authorization header is not sent with the OPTIONS pre-flight, even though I explicitly set withCredentials = true.

However, why would IE and Opera still work? Is Chrome more standards-compliant in this regard? Why did it work and then start failing from v37?

EDIT: Chrome dev tools does not show the Content-Type of the request in the dumps above, but here it is from the Network log. The first pic shows the POST when the server auth is disabled, with content type correctly sent as 'application/json'. The 2nd pic is when the auth is enabled, showing the OPTIONS request failing (it seems OPTIONS is always sent with content type 'text/plain'?).

CORS POST with NO auth on server CORS POST with auth enabled on server

Cornel Masson
  • 1,352
  • 2
  • 17
  • 33
  • You've still not shown all response headers. Could you show the full response for the OPTIONS request as well? If you cannot see it in the developer tools, visit `chrome://net-internals/#events`, then make the request, then return to that chrome page and find the URL_REQUEST that describes your request. – Rob W Oct 10 '14 at 10:22
  • When I have a problem with the CORS, it was due to "content-type". And I don't see any content-type in your query. – queval_j Oct 15 '14 at 13:25
  • See my edit above showing network log with Content-Type – Cornel Masson Oct 16 '14 at 15:06

4 Answers4

7

@Cornel Masson, did you solve the problem? I do not understand why your server is asking you to authenticate the OPTIONS request, but I am facing this same issue against a SAP NetWeaver server. I have read the whole CORS specification (I recommend) so I can clarify you some of your doubts.

About your sentence

In the Angular app I explicitly set the following request headers. AFAIK this setting for withCredentials should ensure that credentials are sent even for OPTIONS requests:

  • According to the CORS specification when a user agent (thus, a browser) preflights a request (requests with OPTIONS HTTP method), it MUST exclude the user credentials (cookies, HTTP authentication...) so any OPTIONS request cannot be requested as authenticated. The browser will request as authenticated the actual request (the one with the requested HTTP method like GET, POST...), but not the preflight request.
  • So browsers MUST not send the credentials in OPTIONS request. They will do in actual requests. If you write withCredentials = true the browser should do what I say.

According to your sentence:

It looks like Chrome is pre-flighting all my POSTs due to the content-type: "application/json":

  • The specification also says that a preflight request will be made by the browser when the header is not a "simple header" and here you have what that means:

    A header is said to be a simple header if the header field name is an ASCII case-insensitive match for Accept, Accept-Language, or Content-Language or if it is an ASCII case-insensitive match for Content-Type and the header field value media type (excluding parameters) is an ASCII case-insensitive match for application/x-www-form-urlencoded, multipart/form-data, or text/plain.

  • application/json is not included so the browser MUST preflight the request as it does.
If anyone finds a solution it would be appreciated.

EDIT: I just found a person with same problem that reflects the real problems, and if you uses the same server as him you will be lucky, https://evolpin.wordpress.com/2012/10/12/the-cors/

Alex MM
  • 306
  • 3
  • 11
  • I haven't found a solution, but a workaround. Since the problem only occurs on **localhost**, I have set up my build process to disable authentication on the server when running on localhost (i.e. my dev PC). For all other deployments (dev server, test, production), authentication is enabled. I don't like this workaround, because I prefer to have complete parity between what I develop on and what is deployed. To mitigate the risk, I always deploy first to a local VM that has auth switched on to do final smoke tests before release. – Cornel Masson Feb 02 '15 at 07:46
  • About your workaround, there is one thing I don't understand. If your local development server is in localhost as your development app, you wouldn't need to disable the authentication so that your app and your server would be in the same domain (localhost), so CORS would not be needed (it comes into play when accessing resources from a different domain than the app). – Alex MM Feb 04 '15 at 07:53
  • it's on a different _port_ on localhost. CORS applies as soon as any component of the URL differs, i.e. scheme (e.g. http://), domain (e.g. www.example.com) or port (e.g. 8081 vs 8080). The problem is that for the development of a one-page Javascript client, the developer's IDE or other dev tools typically allow the quick serving of the app from an embedded web server on its own port, which is different to the port that the local REST server (a different project/application) is listening on. A very common problem for devs, on localhost. – Cornel Masson Feb 05 '15 at 08:34
  • I understand now, thanks. I am facing this same issue but letting the authentication enabled. One aproximation is to use a reverse proxy but then, the proxy must be listening in the same port as the app web server, so I need to find a server that can be both things (web server and reverse proxy). I have been talked that Jetty can do this. Maybe you want to have a try. Good lucky – Alex MM Feb 05 '15 at 11:58
2

Same here. I use Windows NTLM Authentication. Up until Chrome version 37 it worked OK. On versions 37, 38 it fails with 401 (Unauthorized) due to request Authorization header missing in pre-flight OPTIONS on both PUT, and POST.

Server side is Microsoft Web Api 2.1. I tried various CORS including latest NuGet package from Microsoft to no avail.

I have to workaround on Chrome by sending GET request instead of POST, and breaking rather huge data in multiple requests, since URL has a limit in size naturally.

Here are Request/Response headers:


Request URL: http://localhost:8082/api/ConfigurationManagerFeed/
Method: OPTIONS
Status: 401 Unauthorized

Request Headers
Accept: */*
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8,ru;q=0.6
Access-Control-Request-Headers: accept, content-type
Access-Control-Request-Method: POST
Connection: keep-alive
Host: localhost:8082
Origin: http://localhost:8383
Referer: http://localhost:8383/Application/index.html
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36
X-DevTools-Emulate-Network-Conditions-Client-Id: 49E7FC68-A65C-4318-9292-852946051F27

Response Headers
Cache-Control: private
Content-Length: 6388
Content-Type: text/html; charset=utf-8
Date: Fri, 24 Oct 2014 13:40:07 GMT
Server: Microsoft-IIS/7.5
WWW-Authenticate: Negotiate
NTLM
X-Powered-By: ASP.NET
George
  • 21
  • 1
  • 1
    For now, I get around this by disabling HTTP BASIC auth when testing on my localhost. In production the webapp is served from the same host/port as the backend, so no CORS problem there. – Cornel Masson Oct 27 '14 at 08:15
  • The Web Api I work on is used by multiple application; SharePoint and some Mobile apps are among them, so hosting at the same web site is not good solution. Funny thing is that Fiddler "fixes" the requests from Chrome, since Fiddler proxies the requests. So in your case you can work around using Fiddler on development boxes. – George Oct 27 '14 at 15:34
  • Yeah, that's a very valid production scenario and irritating Chrome bug. I haven't considered just proxying everything always, that might be an idea, thanks. – Cornel Masson Oct 29 '14 at 07:38
2

On MS IIS I implemented another workaround by overriding Microsoft standard page life cycle, i.e. processing OPTIONS right at the beginning of the HTTP request in global.ascx:

public class Global : HttpApplication
{
    /// <summary>Check and cofigure CORS Pre-flight request on Begin Request lifecycle
    /// </summary>
    protected void Application_BeginRequest()
    {
        if (Request.Headers.AllKeys.Contains(CorsHandler.Origin) && Request.HttpMethod == "OPTIONS")
        {
            Response.StatusCode = (int)HttpStatusCode.OK;
            Response.Headers.Add(CorsHandler.AccessControlAllowCredentials, "true");
            Response.Headers.Add(CorsHandler.AccessControlAllowOrigin, Request.Headers.GetValues(CorsHandler.Origin).First());
            string accessControlRequestMethod = Request.Headers.GetValues(CorsHandler.AccessControlRequestMethod).FirstOrDefault();
            if (accessControlRequestMethod != null)
            {
                Response.Headers.Add(CorsHandler.AccessControlAllowMethods, accessControlRequestMethod);
            }
            var hdrs = Request.Headers.GetValues(CorsHandler.AccessControlRequestHeaders).ToList();
            hdrs.Add("X-Auth-Token");
            string requestedHeaders = string.Join(", ", hdrs.ToArray());
            Response.Headers.Add(CorsHandler.AccessControlAllowHeaders, requestedHeaders);
            Response.Headers.Add("Access-Control-Expose-Headers", "X-Auth-Token");
            Response.Flush();
        }
    }
}
George
  • 21
  • 1
2

We were experiencing this same issue when trying to debug an Angular 4 front-end application running on localhost:4200 (using the Angular CLI Live Development Server). The Angular application makes http requests to an ASP .Net WebApi2 application running on localhost (IIS Web server) with Windows Authentication. The issue only occurred when making a POST request in Chrome (even though our WebApi is configured for CORs). We were able to temporarily workaround the issue by starting up Fiddler and running a reverse proxy until we found this post (Thank You).

The error message in Chrome debugger: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource

Adding the below code to our Web Api2 - Global.asax file resolved this problem.

If you are looking for the "CorsHandler", you can simply replace George's post with the hard-coded string values, as follows:

    protected void Application_BeginRequest()
    {
        if (Request.Headers.AllKeys.Contains("Origin") && Request.HttpMethod == "OPTIONS")
        {
            Response.StatusCode = (int)HttpStatusCode.OK;
            Response.Headers.Add("Access-Control-Allow-Credentials", "true");
            Response.Headers.Add("Access-Control-Allow-Origin", Request.Headers.GetValues("Origin").First());
            string accessControlRequestMethod = Request.Headers.GetValues("Access-Control-Request-Method").FirstOrDefault();
            if (accessControlRequestMethod != null)
            {
                Response.Headers.Add("Access-Control-Allow-Methods", accessControlRequestMethod);
            }
            var hdrs = Request.Headers.GetValues("Access-Control-Request-Headers").ToList();
            hdrs.Add("X-Auth-Token");
            string requestedHeaders = string.Join(", ", hdrs.ToArray());
            Response.Headers.Add("Access-Control-Allow-Headers", requestedHeaders);
            Response.Headers.Add("Access-Control-Expose-Headers", "X-Auth-Token");
            Response.Flush();
        }
    }

Kind Regards,

Chris