'Access-Control-Allow-Origin issue with CORS and HTTPS redirect (IIS / IISNode / NodeJS)

I have a web application that is in two parts:

  • The "front-end" based in Angular (under Chrome) running on localhost:8000
  • The "back-end" based in ExpressJS/NodeJS, running on localhost:3000

In trying to gradually convert the application to entirely use HTTPS, I thought it would be better to convert the back-end, first. I have built it so that I can toggle the ability to enable/disable HTTPS on the back-end.

I have set up the back-end to run:

  • With two bindings under IIS: http and https
  • The NodeJS application under IISNode.

The problem comes about when I attempt to run the entire application (front- and back-end) under localhost in a local development environment, but with an HTTP-to-HTTPS rewrite (redirect) rule. After that I receive CORS errors on the front-end.

In short, in my local development environment, I am attempting:

Basic App

After reading on all things CORS for hours, I adjusted the web.config and the applicationHost.config based upon this blog post and this StackOverflow article in an attempt to capture the Request Origin header value. Here is what they look like.

My applicationHost.config contains this section:

<location path="Default Web Site">
    <system.webServer>
        <rewrite>
            <allowedServerVariables>
                <add name="CAPTURED_ORIGIN" />
                <add name="RESPONSE_Access-Control-Allow-Origin" />
            </allowedServerVariables>
        </rewrite>
    </system.webServer>
</location>

And here is my Web.config:

<?xml version="1.0" encoding="utf-8"?>
<!--
     This configuration file is required if iisnode is used to run node processes behind
     IIS or IIS Express.  For more information, visit:

     https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config
-->

<configuration>
  <system.webServer>
    <handlers>

      <!-- Indicates that the server.js file is a node.js site to be handled by the iisnode module -->
      <add name="iisnode" path="bin/www" verb="*" modules="iisnode" />
    </handlers>   
    <rewrite>
      <rules>
        <rule name="Capture Origin Header"> 
            <match url=".*" /> 
            <conditions> 
              <add input="{HTTP_ORIGIN}" pattern=".+" /> 
            </conditions> 
            <serverVariables> 
              <set name="CAPTURED_ORIGIN" value="{C:0}" /> 
            </serverVariables> 
            <action type="None" /> 
        </rule>

        <!-- HTTP-to-HTTPS redirect -->
                <rule name="Redirect to HTTPS" enabled="true" stopProcessing="true">
                    <match url="(.*)" ignoreCase="true" />
            <conditions logicalGrouping="MatchAll" trackAllCaptures="true">
                        <add input="{HTTPS}" pattern="^off$" />
                        <add input="{HTTP_HOST}" pattern="([^/:]*?):[^/]*?" />
            </conditions>                   
                      <action type="Redirect" url="https://{C:1}:3443/{R:0}" appendQueryString="true" redirectType="Temporary" />
                </rule>

        <!-- First we consider whether the incoming URL matches a physical file in the /public folder -->
        <rule name="StaticContent">
          <action type="Rewrite" url="public{REQUEST_URI}" />
        </rule>

        <!-- All other URLs are mapped to the node.js site entry point -->
        <rule name="DynamicContent">
          <conditions>
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True" />
          </conditions>
          <action type="Rewrite" url="bin/www" />
        </rule>
      </rules>
      <outboundRules> 
        <rule name="Set-Access-Control-Allow-Origin for known origins"> 
          <match serverVariable="RESPONSE_Access-Control-Allow-Origin" pattern=".+" negate="true" /> 
          <action type="Rewrite" value="{CAPTURED_ORIGIN}" />
        </rule> 
      </outboundRules>       
    </rewrite>

    <!-- Make sure error responses are left untouched -->
    <httpErrors existingResponse="PassThrough" />

  </system.webServer>
  <appSettings>
    <add key="HTTPS_ENABLED" value="true" />
  </appSettings>
</configuration>

The NodeJS application is also set up to handle CORS from localhost:8000, localhost:3000, localhost:3443 (local https), and "null" (converted to a string). (More on that, later.)

But if I use this configuration, then I get the following error in the front-end:

XMLHttpRequest cannot load http://localhost:3000/foo/bar/ Response for preflight is invalid (redirect)

I suspect this is because IIS handled the redirect, but as a result it is handling the preflight check (HTTP OPTIONS) with an invalid response (redirect). However, according to this StackOverflow article, and the answer by @sideshowbarker, leads me to believe that the current version of Chrome, 59.0.3071.104, should be able to handle the HTTP redirect response from the CORS preflight OPTIONS request.

If I remove the server variables from the applicationHost.config and the HTTP-to-HTTPS rewrite and other rules and from the web.config, and then add code to allow the NodeJS application to handle the redirect to HTTPS, then the revised application looks like this:

Revised App

Then it appears an unknown (NodeJS? IIS?) server error occurs because the request is cancelled:

Chrome Dev Tools Networking 1

Chrome Dev Tools Networking 2

You can see the cancellation in chrome://net-internals/#events even though the Origin in the request and the Access-Control-Allow-Origin in the response headers match:

Chrome net-internals

There is no useful error message (even though one is being received by the client) which leads me to believe that it is IIS and not NodeJS that is cancelling the request and sending back no useful information.

I ended up adding a "null" entry to handle CORS when running under the NodeJS Lite Server (and not IIS as an experiment), but I need this to run under IIS/IISNode. However, there seems to be a problem with then IIS / IISNode / NodeJS combination.

I suspect that the Request Origin of "null" is most likely the result of a request, where the server performs a redirect, because you really have two requests: - The original request from the browser - The request that is the result of the redirect When the redirect occurs, I am hypothesizing that the origin in the redirected request is not that same as the original URL, and for the reasons stated in https://www.w3.org/TR/cors/#generic-cross-origin-request-algorithms, the Origin request header within the redirect is null.

However, that doesn't explain, why, when I let NodeJS handle the redirect that the Origin request and the Access-Control-Allow-Origin response header values are both null and the request still gets cancelled. :-/

Finally, if I eliminate any attempt at HTTP-to-HTTPS redirect, then the application works without issue in my development environment.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source