3

I'm stuck trying to diagnose a problem where a GET request with HTTP Basic Auth succeeds in Node, but fails in the browser. The problem manifests directly as a CORS failure (there's no Access-Control-Allow-Origin on the 401 page).

request.js:119 OPTIONS http://HOST?PARAMS 401 (Unauthorized)
ClientRequest._onFinish @ request.js:119
(anonymous) @ request.js:61
...

17:53:59.170 localhost/:1 Failed to load http://HOST?PARAMS: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:9966' is therefore not allowed access. The response had HTTP status code 401. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

The request looks like this:

const request = require("request-promise");
const options = {"url":"http://...?","auth":{"user":"...","pass":"..."},"json":true,"qs":{...},"headers":{},"resolveWithFullResponse":true}
request.get(options).then(...);

How this request shows up in Chrome:

enter image description here

What's surprising here to me:

  • The method is OPTIONS, not GET. (Which I didn't explicitly ask for.)
  • My auth credentials are not included in the headers (even though I haven't set sendImmediately: false
  • The "Provisional headers are shown" warning.

What I would like to know:

  • Is this expected behaviour?
  • If not, what's going wrong?
  • If yes, what's going wrong? :)

Running what would be the equivalent request on the command line seems to work fine:

curl -I 'http://MYUSERNAME:MYPASSWORD@SAMEURL?SAME=PARAMETERS' -H 'Origin: http://localhost:4466' -X OPTIONS
HTTP/1.1 200 OK
Date: Wed, 20 Jun 2018 07:29:28 GMT
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: http://localhost:4466
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Access-Control-Allow-Origin,Access-Control-Allow-Credentials
Vary: Origin
X-Frame-Options: SAMEORIGIN
Allow: GET,HEAD,POST,OPTIONS
Content-Length: 0

However I notice that without the credentials supplied in the URL, there are no CORS headers on the response:

HTTP/1.1 401 Unauthorized
Date: Wed, 20 Jun 2018 07:45:26 GMT
Server: Apache/2.4.29 (Unix) OpenSSL/1.0.2l
WWW-Authenticate: Basic realm="Authentication Required"
Content-Length: 381
Content-Type: text/html; charset=iso-8859-1

Is that the correct behaviour?

Hypothesis

I suspect:

  • The browser is sending the OPTIONS "pre-flight" request because it has to, because the request is not a "simple request". (Not totally clear if this is the request library or the browser itself doing this)
  • The server is responding 401 because credentials weren't supplied, but isn't adding the CORS headers that it should.
  • Without the CORS headers the browser prevents the Request library accessing the result, so it can't complete the authentication.

What I'm not sure about:

  • Is the server misconfigured?
  • Is Request failing to pass credentials in the OPTIONS preflight request?
Steve Bennett
  • 114,604
  • 39
  • 168
  • 219
  • The server is misconfigured. It’s not properly CORS-enabled. To be properly CORS-enabled it must respond to unauthenticated OPTIONS requests with a 200 or 204 status code. Request is not “failing” to pass credentials in the OPTIONS preflight request. Request isn’t even making the the OPTIONS request. Instead the browser engine is, on its own. And the CORS spec requires browsers to omit all credentials from the OPTIONS requests. So that behavior is intentional, by design. See the detailed explanation in the answer https://stackoverflow.com/a/45406085/441757 – sideshowbarker Jun 20 '18 at 16:28
  • Thanks, that's awesome. Would you like to make that an answer so I can accept it? – Steve Bennett Jun 21 '18 at 00:13

1 Answers1

4

The server is misconfigured. It’s not properly CORS-enabled. To be properly CORS-enabled it must respond to unauthenticated OPTIONS requests with a 200 or 204 status code.

Request isn’t “failing” to pass credentials in the OPTIONS preflight request. Request isn’t even making the the OPTIONS request. Instead the browser engine is, on its own.

And specifying the credentials flag for the request in your own code doesn’t cause credentials to get added to the preflight OPTIONS request. Instead, credentials only get added to the GET request from your own code — that is, if the preflight has succeeded and the browser actually ever moves on to making that GET request.

That’s because the CORS-protocol requirements in the Fetch spec state that browsers must omit all credentials from preflight OPTIONS requests.

See https://fetch.spec.whatwg.org/#ref-for-credentials%E2%91%A5:

a CORS-preflight request never includes credentials

So the behavior you’ve encountered is intentional, by design.

For a related explanation in more detail, see the answer at HTTP status code 401 even though I’m sending credentials in the request.

sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
  • Thanks. So the "with credentials" basically serves to say "yes, I really truly do want to send these credentials to this site hosted on a different domain", which otherwise the browser would stop you doing? – Steve Bennett Jun 21 '18 at 03:31
  • Yes. Or to put it other words, by default the browser omits credentials. In your client code, you must explicitly opt in to having the browser send them. That’s what the credentials flag does. And the server you’re making the request to must also opt in to telling the browser it’s actually OK to expose the credentials from the response to your frontend JavaScript code. That’s what the Access-Control-Allow-Credentials response header is for. For a more detailed explanation of that, see the answer at https://stackoverflow.com/a/45004550/441757 – sideshowbarker Jun 21 '18 at 03:44
  • Super helpful. Wish I could give you more than this meagre upvote! :) – Steve Bennett Jun 21 '18 at 03:48