Adobe

NGINX Proxy for CORS

Istock 641204328
In my latest AEM project, I had a particular challenge with some front-end code, an AJAX call, and CORS.
 
While running the site on localhost, the search code attempted to make an AJAX call to a search API. The API would not allow my localhost site. This meant that front-end developers had a hard time doing local testing. To solve this, I set up a proxy server to fool the browser into thinking CORS was set up for my site running on localhost.

What is CORS

CORS stands for cross-origin resource sharing. It is a mechanism by which the server will control access to its goodies, should that someone be running on a different domain.

  • It occurs between the browser and a server (usually some sort of API endpoint).
  • The browser sends some information via HTTP Access-Control-Request-* headers.
  • The server makes a determination on headers received a sends back Access-Control-Allow-* headers.
  • The browser now knows if it can or can not access resources on this server.
  • In some circumstances, the browser may do a pre-flight which is a test prior to doing the real request.
  • The browser will not allow front-end code to modify the headers.
  • The server-side code can change the headers. But, it should be transparent to the application and done by a downstream service such as an API gateway or the HTTP server.

The Access-Control-Request-* CORS headers tell the server

  • My current domain (Origin header)
  • HTTP method of the request (Access-Control-Request-Method header)
  • List of headers in the request (Access-Control-Request-Headers header)

The Access-Control-Allow-* CORS headers tell the browser

  • The origin that is allowed (Access-Control-Allow-Origin header)
  • The methods that are allowed (Access-Control-Allow-Methods header)

This is the heart of CORS. If the backend service does not send back Access-Control-Allow-* headers with correct values, the browser will not allow the request to continue.

I like to think of the entire exchange as a Gentlemen’s Agreement. The gentlemen are the browser and the server. Why? Because it is an informal exchange of data that depends on the honesty of both parties. You can break the agreement simply by fudging with the headers.

Bypassing CORS

All we need to do is fool the browser and/or the service so that the AJAX request can proceed. Perhaps your browser has security switches you can flip. You can probably find a plugin to do the trick. Adding a host file entry so you can run your local site on the allowed domain may work. The best way I could think of would be to set up a proxy server to sit between the front-end code and back-end services.

Always be wary of proxy servers. After all, they can inspect your traffic and make changes to your payloads. In our case, we are going to toggle the required CORS headers to make the browser and API happy. And we will do this from within the safety of a local Docker container so we will never actually send out any data.

Setting Up A Service

If you already have a backend service with CORS enabled, great! For this test, we are going to set up a quick and dirty express service.

You will need to add this to your hosts’ file:

  • 127.0.0.1 mybackend.com

The server.js file

const express = require('express');
var path = require('path');
const cors = require('cors');
const corsOpts = {
  origin: /myfrontend.com:3000/,
  allowedHeaders: []
}
const app = express();
app.get('/service', cors(corsOpts), (req, res) => {
        res.send(`hello, ${req.get('origin')}`);
        console.log(JSON.stringify(req.headers, null, '  '));
    })
   .get('/', cors(corsOpts), (req, res) => {
        res.sendFile(path.join(__dirname + '/index.html'));
    })
   .options('*', cors(corsOpts));

const server = app.listen(3000, () => {
    console.log(`server listening on port ${server.address().port}`);
});
Adobe - Content for Everyone
Content for Everyone

Companies that can quickly and consistently meet the demands of consumers are thriving in an era of infinite content. Learn about how to build fluid experiences for your omnichannel customers.

Get the Guide

The index.html file

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>CORS</title>
</head>

<body>
    Response: <span id="response"></span>
</body>

<script type="text/javascript" src="//code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
    $(function() {
        $.ajax({
            url: 'http://mybackend.com:3000/service',
            success: res => {
                $('#response').text(res);
            },
            error: err => {
                $('#response').text(err.statusText);
            }
        })
    });
</script>

</html>

And when you fire it up, you can access http://localhost:3000/. Notice the annoying CORS error in the browser’s console

Access to XMLHttpRequest at 'http://mybackend.com:3000/service' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Luckily, CORS errors on the browser are self-explanatory. The browser is saying, “I can’t allow this because the server did not inform me that the current origin is allowed to make this request.”

I mentioned earlier there are a few fixes:

  • You can add 127.0.0.1 myfrontend.com to your hosts’ file and access the site via http://myfrontend.com:3000/ but editing the hosts’ file can be impractical or sometimes impossible.
  • You can change server.js to allow localhost however we do not always have control over the services we integrate with.
  • You can install a browser plugin such as Allow CORS: Access-Control-Allow-Origin in Chrome.
  • You can add CORS Anywhere into your project if you have JavaScript skills.

Setting up an NGINX Proxy

For my particular project, Docker was already part of the local developer environment. All I needed to do was add an extra service to the docker-compose file and a couple of config files. The proxy sat there, transparent to developers. On the local run mode, the font-end code would integrate with the remote service via the local proxy. On all other run modes, it would integrate with the remote service.

The docker-compose.yml file

version: '3.8'

services:

  proxy:
    image: nginx:alpine
    ports:
      - 8080:80
    volumes:
      - ./default.conf.template:/etc/nginx/templates/default.conf.template

The default.conf.template file:

server {
    listen 80 default_server;
    server_name _;
    server_name_in_redirect off;

    access_log  /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log debug;

    location /service {
        proxy_pass http://host.docker.internal:3000;
    }
}

Make sure to update the URL on the JavaScript so it uses the proxy running on 8080:

$(function() {
    $.ajax({
        //url: 'http://mybackend.com:3000/service',
        url: 'http://mybackend.com:8080/service',
        success: res => {
            $('#response').text(res);
         },
        error: err => {
            $('#response').text(err.statusText);
        }
    })
});

Fire up the docker-compose service and access http://localhost:8080/. You still see the same CORS error but now to the proxied service on port 8080.

Modifying Requests and Responses

The error message is saying you are missing the Access-Control-Allow-Origin header. The service will return this header if the Origin matches the pattern /myfrontend.com:3000/. Let’s fool the service by supplanting the Origin header before sending it upstream

location /service {
    proxy_pass http://host.docker.internal:3000;
    proxy_set_header Origin http://myfrontend.com:3000;
}

The service will send back the Access-Control-Allow-Origin header and the CORS error changes

Access to XMLHttpRequest at 'http://mybackend.com:8080/service' from origin 'http://localhost:3000' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'http://myfrontend.com:3000' that is not equal to the supplied origin.

The browser is saying the service will only allow http://myfrontend.com:3000 and you are on localhost. Let’s fool the browser by supplanting the Access-Control-Allow-Origin header.

location /service {
    proxy_pass http://host.docker.internal:3000;
    proxy_set_header Origin http://myfrontend.com:3000;
    proxy_hide_header Access-Control-Allow-Origin;
    add_header Access-Control-Allow-Origin $http_origin;
}

First, we hide it. Then we add our own before sending back the response. NGINX will not replace headers; it will append to them. If you don’t hide it first, you’ll wind up with 2 origins and this will cause another CORS error. Like I said before, CORS errors on the browser are self-explanatory. Try it.

Something to Try on Your Own

I mentioned earlier pre-flight requests. To trigger one, update the AJAX call to add a header.

$(function() {
    $.ajax({
        headers: { 'X-Foo': 'bar' },
        url: 'http://mybackend.com:8080/service',
        success: res => {
            $('#response').text(res);
         },
        error: err => {
            $('#response').text(err.statusText);
        }
    })
});

In this example, we will add the X-Foo header. You’ll note that our service implementation allows no extra headers. The correct thing would be to update the service implementation. If you have no control over that you can leverage the proxy.

Look at the CORS error on the browser. Can you update the response at the proxy to fool the browser?

Conclusion

We set up a simple example to fool the browser and service into allowing the cross-origin request. Depending on your particular needs you may need to make extra changes. At the very least you now have a testing app and proxy.

About the Author

Juan Ayala is a Lead Developer in the Adobe practice at Perficient, Inc., focused on the Adobe Experience platform and the things revolving around it.

More from this Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up