9

I have a site with two https servers. One (frontend) serves up a UI made of static pages. The other (backend) serves up a microservice. Both of them happen to be using the same (test) X509 certificate to identify themselves. Individually, I can connect to them both over https requiring the client certificate "tester".

We were hiding CORS issues until now by going through an nginx setup that makes the frontend and backend appear that they are same Origin. I have implemented the headers 'Access-Control-Allow-Origin', 'Access-Control-Allow-Credentials' for all requests; with methods, headers for preflight check requests (OPTIONS).

  • In Chrome, cross-site like this works just fine. I can see that front-end URLs and backend URLs are different sites. I see the OPTIONS requests being made before backend requests are made.

  • Even though Chrome doesn't seem to need it, I did find the xmlhttprequest object that will be used to perform the request and did a xhr.withCredentials = true on it, because that seems to be what fetch.js does under the hood when it gets "credentials":"include". I noticed that there is an xhr.setRequestHeader function available that I might need to use to make Firefox happy.

    • Firefox behaves identically for the UI calls. But for all backend calls, I get a 405. When it does this, there is no network connection being made to the server. The browser just decided that this is a 405 without executing any https request. Even though this is different behavior from Chrome, it kind of makes sense. Both the front-end UI and backend service need a client certificate to be chosen. I chose the certificate "tester" when I connected to the UI. When it goes to make a backend request, it could assume that the same client certificate should be used to reach the back-end. But maybe it assumes that it could be different, and there is something else I need to tell Firefox.

Is anybody here using CORS in combination with 2 way SSL certificates like this, and had this Firefox problem and fixed it somewhere. I suspect that it's not a server-side fix, but something that the client needs to do.

Rob
  • 1,247
  • 1
  • 12
  • 16
  • Have you tested in Safari? Edge? It would be interesting to know of they matches Firefox on this. It may be the behavior you're seeing only happens in Chrome. The Firefox behavior seems to be in conformance with the current spec. – sideshowbarker Mar 08 '17 at 04:44
  • I am having the same problem and we also use a 2-way SSL setup. Except I don't get any status code -- Firefox simply aborts the ajax call. Did you ever get this resolved? – heez Mar 15 '18 at 15:12
  • Basically, it's a bug that it even works in Chrome. The spec says that an OPTION verb should not use the client certificate when it makes the request. If you reject all connections without a client cert, then you can never get the OPTIONS request. You could make nginx send back a 200 for you and keep your service from seeing OPTION though. That is what I will try, because CORS is almost useless if it isn't portable. https://bugzilla.mozilla.org/show_bug.cgi?id=1019603#c9 – Rob Mar 17 '18 at 01:43
  • 1
    The problem with the spec's requirement is that the browser knows what type of request it is before it sends it, but the server doesn't know until *after* the TLS negotiation – kbolino Mar 27 '18 at 19:51
  • I raised https://bugs.chromium.org/p/chromium/issues/detail?id=775438 for this (a couple years ago). The Chrome behavior hasn’t been changed yet, though the comments in that bug indicate they do intend to change it. – sideshowbarker May 30 '20 at 17:29

2 Answers2

2

I haven't actually tested this using client certificates, but I seem to recall that Firefox will not send credentials if Access-Control-Allow-Origin is set to the * wildcard instead of an actual domain. See this page on MDN.

Also there's an issue with Firefox sending a CORS request to a server that expects the client certificate to be presented in the TLS handshake. Basically, Firefox will not send the certificate during the preflight, creating a chicken and the egg problem. See this bug on bugzilla.

AfroThundr
  • 1,017
  • 2
  • 17
  • 26
  • 2
    The comment from a Chrome committer at https://bugzilla.mozilla.org/show_bug.cgi?id=1019603#c9 suggests the bug here is actually in Chrome and that the Firefox behavior is what the current spec requires. So it would be interesting to know what Safari and Edge do here. – sideshowbarker Mar 08 '17 at 05:06
  • Indeed. If it turns out that the Firefox interpretation is correct, how then would one implement client certificate authentication on the backend? Maybe by using anonymous SSL for the initial connection then renegotiating after the preflight? I'm not sure if that would even work. – AfroThundr Mar 08 '17 at 11:25
  • This is the correct answer to the question. The cause of the difference in browser behavior is just a bug in Chrome. Firefox, Edge and Safari all do what the spec requires — which is, they don’t include SSL/TLC client certificates in preflight requests. See the related answer at https://stackoverflow.com/questions/46783506/why-is-the-tls-client-certificate-not-being-included-in-preflight-request-on-mos/46783730#46783730 – sideshowbarker Oct 17 '17 at 07:25
  • 1
    The Firefox behavior cannot possibly be seriously considered. A TLS connection that requires 2-way auth gets setup, and an http protocol layer runs inside of it. More specifically, it's going to be something like an nginx server that is not under any circumstances going to forward an unauthenticated request to a microservice behind it; for auditing reasons. The Chrome behavior is right, no matter what the spec says. – Rob Mar 16 '18 at 04:24
  • 1
    I agree that Chrome's way of dealing with this might be more desirable for use cases like this, but following specs is also important. I don't believe this situation was considered when they wrote the CORS spec, so getting that revised to account for intentional authentication in preflight requests would probably be the more correct solution. – AfroThundr Mar 16 '18 at 11:02
  • @Rob I think something like SNI will be necessary, to indicate before the connection is negotiated that it is a CORS preflight request, otherwise setting up TLS with mandatory client certs gets a lot more complicated on the server side – kbolino Mar 27 '18 at 19:48
0

When using CORS with credentials (basic auth, cookies, client certificate, etc.):

  • Access-Control-Allow-Credentials must be true
  • Access-Control-Allow-Origin must not be *
  • Access-Control-Allow-Origin must not be multi-value (neither duplicated nor comma-delimited)
  • Access-Control-Allow-Origin must be set to exactly the value from the request's Origin header in order for the request to work (either hard-coded that way or if it passes a whitelist of allowed values)
  • The preflight OPTIONS request must not require credentials (including the client certificate). Part of the purpose of the preflight is to ask what is allowed in a CORS request, and therefore sending credentials before knowing if they are allowed is incorrect.
  • The preflight OPTIONS request must return a 200-level response, generally 204

Note: For Access-Control-Allow-Origin, you may want to consider allowing the value null since redirect chains (like the ones typically used for OAuth) can cause that Origin value in a request from a browser.

willsters
  • 148
  • 8
  • Have you actually tried it in the different browsers though? The last time I tried this was about 2 years ago. It works in Chrome with PKI client cert. It does not work in Firefox or Safari. This is because the spec says that if you authenticated with a cert, it won't sent the DN on during an OPTIONS request. And because of that mTLS connection will fail to reach the backend. The reason it works in Chrome is that they (correctly imho) violated the specification, to make the functionality work. It may have changed since I last investigated it. – Rob May 29 '20 at 03:28
  • 1
    Yes. We have ours working in Chrome, Firefox, IE, Safari, and Edge (for about 7-8 years now). There are ways to configure both Apache HTTPD and Nginx to not require client verification for the OPTIONS request and handle anything not OPTIONS as 403 if the certificate is not provided. Doing your main testing for this in Chrome is ill-advised since you are correct that it violates the specification for convenience. – willsters Jun 01 '20 at 14:18
  • ah. in our case, we require mTLS. if you are not identified, the edge simply will not forward to the backend. this is before http parsing can even begin. unfortunately, due to how TLS is layered over http, there are a lot of servers that work like this. – Rob Jun 02 '20 at 16:39
  • Can you not do your CORS on the edge? That's where we do all of ours. It should be possible to at least handle OPTIONS at the edge and let all the others be handled by your backend if you want to do it that way. – willsters Jun 02 '20 at 17:41