Background

Recently, I needed to hand over my previous side project to others, and I started to think about how could I complete it in an easy, simple, and low-communication cost way. That's when the term 'docker-compose' popped into my head. So, I decided to complete the task with 'docker-compoes'.  

Soon enough, problems started cropping up. This side project consists of three services: the client, console, and API service, each with its own corresponding URLs, like these:

  • client: domain-a.com
  • console: console.domain-a.com
  • server: api.domain-a.com

In appearance, this setting will hit the CORS issue, we will briefly introduce the CORS later. To address it, we initially applied a quick fix by adding the 'Access-Control-Allow-Origin *' setting to our Nginx.

However, this solution isn't perfect because we actually want to permit access to the resource from multiple origins only. With 'Access-Control-Allow-Origin *,' essentially, anyone on the internet can attempt to access our resources, which poses more risk.

In this post, I'll share how to allow access from multiple origins to your resources specifically. Let's dive in.

CORS

First, let's get a handle on what CORS is. CORS stands for Cross-Origin Resource Sharing. Why did we need it? Well, it all goes back to the early days when people hadn't quite caught on to this problem. See, hackers would attempt to create phishing websites, like those horoscope or farm news sites you might have seen. Sneakily tucked behind these innocent-looking sites, they'd slip in some script code to access resources from other websites using your browser's cookies. This is what's known as CSRF.

CORS, along with the same-site attribute for cookies, can effectively prevent CSRF on the browser side. I won't delve deeper into this aspect; if you're interested, there are plenty of resources available on the internet.

Now, returning to CORS, what does the error actually look like?

Why? How does the browser figure out that a request has hit the CORS limitation? Well, the browser breaks down the URL into three parts: the scheme, domain, and port. When all three components match, it's considered the 'same origin.' Here are some examples:

https://domain-a.com is my website.

  • schema: https
  • domain: domain-a
  • port: 443

Next, let's examine whether the following request complies with CORS.

  • http://domain-a.com → not the same origin (schema is not the same)
  • https://domain-a.com/mike → same origin.
  • https://news.domain-a.com → not the same origin (domain is not the same)
  • https://domain-a.com:81 → not the same origin (port is different)
  • https://domain-b.com → not the same origin (domain is not the same)

By this point, you should have a basic understanding of what CORS is. Next section, I will show you how to solve it via Nginx.

Nginx Setting

Most of the time, we opt for Nginx as our web server due to its various advantages, like being lightweight, simple, and stable, among others. Now, considering our prior understanding of CORS rules, how does the server manage which requests can access its resources?

This is where the Access-Control-Request-*headers come into play. The server determines what to send back as Access-Control-Allow- headers. Based on these headers, the browser makes the call on which requests can bypass the CORS restriction.

Here is the simplest solution you can find on the internet.

location / {
    
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    # the remaning setthings
    ...
    ...
    ...
}

This is clearly not what we want. So, here's another solution that I've come up with recently. We employ a preflighted requests handler to communicate to the browser under which conditions we can permit CORS. Typically, the Access-Control-Allow-Origin header only accepts a single origin value. Unfortunately, for our side project, we need to allow multiple origins. To meet this requirement, we utilize the 'map' function in Nginx.

According to the Nginx website, the map function is used to create a new variable whose value depends on the values of one or more of the source variables specified in the first parameter. For more details, you can check this link.

map $http_origin $allowed_origin {
    ~*^http://localhost:3030$      $http_origin;
    ~*^http://localhost:4000$      $http_origin;
}

location / {

    # Preflighted requests
    if ($request_method = OPTIONS) {
        add_header "Access-Control-Allow-Origin" $allowed_origin;
        add_header "Access-Control-Allow-Methods" "GET, PUT, POST, DELETE, OPTIONS";
        add_header "Access-Control-Allow-Headers" "Authorization, Content-Type";
        add_header "Access-Control-Max-Age" 86400;
        return 204;
    }

    if ($allowed_origin != "") {
        add_header "Access-Control-Allow-Origin" $allowed_origin;
    }

    # the remaning setthings
    ...
    ...
    ...
}

By doing this, we can greenlight several origins to navigate through the CORS barrier when working locally. What's even more crucial is that we can seamlessly run multiple services and have them communicate with the API service via localhost with different ports using docker-compose.

Summary

In this post, we've presented a solution that enables multiple origins to navigate the CORS restriction in a more secure way. We'd like to reiterate the importance of avoiding the wildcard solution Access-Control-Allow-Origin * in your Nginx configuration, and hope this post is helpful to you, thanks.

Reference