8

I'm having an issue with a web app I'm building. The web app consists of an angular 4 frontend and a dotnet core RESTful api backend. One of the requirements is that requests to the backend need to be authenticated using SSL mutual authentication; i.e., client certificates.

Currently I'm hosting both the frontend and the backend as Azure app services and they are on separate subdomains.

The backend is set up to require client certificates by following this guide which I believe is the only way to do it for Azure app services: https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth

When the frontend makes requests to the backend, I set withCredentials to true — which, [according to the documentation][1], should also work with client certificates.

The XMLHttpRequest.withCredentials property is a Boolean that indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests.

Relevant code from the frontend:

const headers = new Headers({ 'Content-Type': 'application/json' });
const options = new RequestOptions({ headers, withCredentials: true });

let apiEndpoint = environment.secureApiEndpoint + '/api/transactions/stored-transactions/';

return this.authHttp.get(apiEndpoint, JSON.stringify(transactionSearchModel), options)
    .map((response: Response) => {
         return response.json();
     })
     .catch(this.handleErrorObservable);

On Chrome this works, when a request is made the browser prompts the user for a certificate and it gets included in the preflight request and everything works.

For all the other main browsers however this is not the case. Firefox, Edge and Safari all fail the preflight request because the server shuts the connection when they don't include a client certificate in the request.

Browsing directly to an api endpoint makes every browser prompt the user for a certificate, so I'm pretty sure this is explicitly relevant to how most browsers handle preflight requests with client certificates.

Am doing something wrong? Or are the other browsers doing the wrong thing by not prompting for a certificate when making requests?

I need to support other browsers than Chrome so I need to solve this somehow.

I've seen similar issues being solved by having the backend allow rather than require certificates. The only problem is that I haven't found a way to actually do that with Azure app services. It's either require or not require.

Does anyone have any suggestions on how I can move on?

  • Is the request made using `XMLHttpRequest()` or `fetch()`? – guest271314 Oct 17 '17 at 06:21
  • If the request is working as expected in Chrome, the please use https://stackoverflow.com/posts/46783506/edit to edit/update the question and add the request and response details for the Chrome OPTIONS request (as the question already has for the Firefox request) and for the subsequent POST request. – sideshowbarker Oct 17 '17 at 06:29
  • @sideshowbarker You are right, I updated the question with the relevant screen caps. – Linus Eiderström Swahn Oct 17 '17 at 06:35
  • @guest271314 According to the angular 4 documentation (angular.io/api/http/Http) it is XMLHttpRequest. – Linus Eiderström Swahn Oct 17 '17 at 06:36
  • Your Chrome screen capture shows an OPTIONS request for a GET request, and a subsequent GET request — not the POST request from your code. – sideshowbarker Oct 17 '17 at 06:38
  • If request is an `XMLHttpRequest()` what is the purpose of `const headers = new Headers({ 'Content-Type': 'application/json' });`? See [Why does Fetch API Send the first PUT request as OPTIONS](https://stackoverflow.com/questions/42311018/why-does-fetch-api-send-the-first-put-request-as-options/) – guest271314 Oct 17 '17 at 06:39
  • @sideshowbarker You are right. I'm sorry, I included the wrong request. Updated with the actual request being sent. – Linus Eiderström Swahn Oct 17 '17 at 06:45
  • To be clear about something: You mention TLS client certificates but the details shown for the OPTIONS and GET requests show that your frontend code is actually adding an Authorization request header — for HTTP authentication, which is not the same as a TLS client certificate. So is your code trying to use *both* HTTP authentication with the Authorization header *and* a TLS client certificate? – sideshowbarker Oct 17 '17 at 06:54
  • As alluded to in another comment here, you should remove the `const headers = new Headers({ 'Content-Type': 'application/json' })` part from your code. Adding a Content-Type header to a GET request isn’t necessary or useful, since the request has no payload/body. – sideshowbarker Oct 17 '17 at 06:56
  • @sideshowbarker Yes, basically requests are authorized with jwt tokens. In addition requests also need to be authenticated using TLS client certificates because of a 3rd party requirement. – Linus Eiderström Swahn Oct 17 '17 at 06:58
  • @sideshowbarker Thanks for the tip, I haven't really reflected on it but you are right of course. – Linus Eiderström Swahn Oct 17 '17 at 07:00
  • OK after looking a bit I rediscover that this is something I remember investigating before, and that the cause of this is a bug in Chrome. See my updated answer. – sideshowbarker Oct 17 '17 at 07:17
  • @LinusEiderströmSwahn can you confirm whether you’re using an enterprise policy which enables you to force-select a given client certificate for all requests for client certs that meet certain domain policies? And whatever your answer to that question is, please consider adding it as a comment yourself at https://bugs.chromium.org/p/chromium/issues/detail?id=775438 – sideshowbarker Oct 17 '17 at 14:22
  • @sideshowbarker How would I see an Enterprise policy? If I enter `chrome://policy/` in the chrome bar It says I have no Policies set. – Linus Eiderström Swahn Oct 18 '17 at 11:01
  • @LinusEiderströmSwahn I don’t know myself how to find out if an enterprise policy has been set. But if you’re able to ask in a comment over at https://bugs.chromium.org/p/chromium/issues/detail?id=775438 I think you’ll get a reply back there with guidance about how to check – sideshowbarker Oct 18 '17 at 11:05

1 Answers1

5

See https://bugzilla.mozilla.org/show_bug.cgi?id=1019603 and my comment in the answer at CORS with client https certificates (I had forgotten I’d seen this same problem reported before…).

The gist of all that is, the cause of the difference you’re seeing is a bug in Chrome. I’ve filed a bug for it at https://bugs.chromium.org/p/chromium/issues/detail?id=775438.

The problem is that Chrome doesn’t follow the spec requirements on this, which mandate that the browser not send TLS client certificates in preflight requests; so Chrome instead does send your TLS client certificate in the preflight.

Firefox/Edge/Safari follow the spec requirements and don’t send the TLS client cert in the preflight.


Update: The Chrome screen capture added in an edit to the question shows an OPTIONS request for a GET request, and a subsequent GET request — not the POST request from your code. So perhaps the problem is that the server forbids POST requests.


The request shown in https://i.stack.imgur.com/GD8iG.png is a CORS preflight OPTIONS request the browser automatically sends on its own before trying the POST request in your code.

The Content-Type: application/json request header your code adds is what triggers the browser to make that preflight OPTIONS request.

It’s important to understand the browser never includes any credentials in that preflight OPTIONS request — so the server the request is being sent to must be configured to not require any credentials/authentication for OPTIONS requests to /api/transactions/own-transactions/.

However, from https://i.stack.imgur.com/GD8iG.png it appears that server is forbidding OPTIONS requests to that /api/transactions/own-transactions/. Maybe that’s because the request lacks the credentials the server expects or maybe it’s instead because the server is configured to forbid all OPTIONS requests, regardless.

So the result of that is, the browser concludes the preflight was unsuccessful, and so it stops right there and never moves on to trying the POST request from your code.

Given what’s shown in https://i.stack.imgur.com/GD8iG.png it’s hard to understand how this could actually be working as expected in Chrome — especially given that no browsers ever send credentials of any kind in the preflight requests, so any possible browsers differences in handling of credentials would make no difference as far as the preflight goes.

sideshowbarker
  • 62,215
  • 21
  • 143
  • 153
  • Thanks for the post. I thought I understood about CORS preflight requests. But I guess that Chrome is actually the culprit and does the wrong thing by including a TLS client certificate in the preflight request also. The server stops the preflight OPTIONS because it requires a TLS client certificates which it doesn't get. I guess the only solution would be to either make the requests served from the saim sub domain or find a way to make Azure take a client certificate as optional so that the OPTIONS request actually gets a response. – Linus Eiderström Swahn Oct 17 '17 at 07:05
  • One of the solutions, I used, is to use iframe and postMessage. Works on all browsers. – nemke Oct 31 '17 at 07:50