How to Set Up Nginx with CORS for Multiple Origins

Setting up Nginx to support Cross-Origin Resource Sharing (CORS) with multiple origins is straightforward when you know the trick. In this article, we'll configure an Nginx server so CORS will never bother you again. Code included.
How to Set Up Nginx with CORS for Multiple Origins

A couple of months ago, I published an article discussing Cross-Origin Resource Sharing (better known among us as CORS). In it, I outlined what CORS is, how it affects you, and how to overcome the connection challenges every developer inevitably encounters at some point 😱.

The explanations were theoretical to help you grasp the concept and workings of this protocol. And in that piece, we explored one of the most crucial CORS headers: the Access-Control-Allow-Origin header. To briefly recap, this header dictates which websites can access the API: if the source of the browser-initiated call didn't match this header's value, the connection was denied. I also mentioned that this header could either specify one origin (if you have multiple origins, a workaround is needed) or use an asterisk * to indicate any origin—a highly insecure approach that isn't recommended.

Remember that the origin header (in English, "origin") is not just the domain you're browsing. The origin consists of the protocol, domain (or subdomain), and port. Therefore, http://juannicolas.eu is not the same origin as https://www.juannicolas.eu, as both the protocol and the www subdomain differ.

From a theoretical perspective, that's clear. But what about practically speaking? How can I configure an Nginx server to adhere to CORS? Can I enable more than one origin in the Access-Control-Allow-Origin header? 🤔

In this article, I'll guide you on how to set up an Nginx web server to fully support CORS and even allow you to include multiple origins in the Access-Control-Allow-Origin header from a functional viewpoint. So, get Nginx up and running on your machine (or in a Docker container for testing), and let's dive in.

Understanding Nginx's Configuration File

First, let's go over the basics for the less experienced developers out there. We'll look at what Nginx is and get a brief overview of how an Nginx web server's configuration file works.

Nginx is an open-source web server responsible for handling client (browser) requests, delivering the relevant information. It also functions as a reverse proxy, cache, and load balancer. Pronounced as engine-x, this software was designed to address the challenge of handling 10,000 concurrent requests.

In simpler terms, when a client (your browser or app) makes a request to your domain, Nginx processes the request and analyses it, returning a static file (a JavaScript file, CSS style, or HTML file). Alternatively, it forwards the request to the appropriate software (a PHP engine, a Node.js instance, etc.). Nginx excels in this role due to its speed and its minimal resource consumption compared to other web servers. As such, it's also employed as an HTML cache (saving the HTML code produced by another software to avoid redundant resource use) and as a load balancer (distributing requests to other systems in a controlled manner).

Like all web servers, Nginx requires a configuration file to know which domains to serve and on which ports. A classic Nginx domain configuration file typically looks something like this:

server {
# SERVER LEVEL CONFIGURATION
listen       80;
server_name  api.mydomain.com;
access_log   logs/api.mydomain.com.access.log  main;

    location ~ ^/(images|javascript|js|css|flash|media|static)/  {
      # LOCATION LEVEL CONFIGURATION
      root    /var/www/virtual/api.mydomain.com/public;
      expires 30d;
    }

    location / {
      # LOCATION LEVEL CONFIGURATION
      proxy_pass      http://127.0.0.1:8080;
    }
}

As you can see, and generally, when we create or enable a website, we create a file containing this basic information. It is true that the larger or more complex the project, the more routes or configurations it will have, but this one is fine to start with.

In this configuration file, we define what the domain that Nginx should serve is called, what type of connection (or where the certificates are hosted in the case of HTTPS) and some more information regarding the server (common to all the routes within it). Subsequently, we define the routes (or rather, a set of routes) in one or several blocks preceded by the keyword location. That is, if the client requests a static resource, look for it in this folder; if it requests the /admin directory, look for it over there; and so on.

So, now that we know the skeleton of an Nginx configuration file, we can move on to the next level: making Nginx attach Cross-Origin Resource Sharing headers.

Including CORS in Nginx with a Single Origin

Recapping, we already have Nginx serving on a domain and ready to serve our web files. The problem we face now is that this Nginx needs to attach CORS headers for our browser requests to work correctly. These headers are the ones we saw in the article I referred to at the beginning: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Expose-Headers. Just to review, these headers indicate from which domain it is valid to make a request, which methods are allowed, what additional headers the request can carry, and which response headers should be available to the APP, respectively.

Let's say Nginx is controlling the domain miapi.es, while our web application is hosted on the domain miapp.es. This is just a visual aid; it could really be any subdomain or domain, but to make the configuration file easier to read, I'll use these simple domains. Also, remember that according to the CORS protocol, in certain situations and before sending the information to the server, the browser makes an initial request to the server to ensure that the server understands the CORS protocol. This request is called a preflight request and uses the HTTP method called options. With that explained, we add the following to our Nginx configuration:

server {
    # Server
    listen       80;
    server_name  api.midominio.com;

    location / {
      # CORS headers
      add_header 'Access-Control-Allow-Origin' 'https://miapp.es' always;
      add_header 'Access-Control-Allow-Credentials' 'true' always;
      add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
      add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
 
      # if preflight request, we will cache it
      if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
      }

       # configuration lines...
    }
}

Done! Our Nginx is now attaching the Cross-Origin Resource Sharing headers. We have authorized the origin https://miapp.es to send GET, PUT, POST, DELETE, and OPTIONS requests, along with the headers Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With.

In theory, we have achieved what we wanted, attaching CORS headers to requests with Nginx. However, we need to put all these instructions in each location block, and to be honest, it looks messy and reads poorly. Since we are good developers who like to have clean and readable configurations, we are going to move the CORS configurations into a snippet (reusable code fragments for Nginx). This way, we can use them in other configurations and keep the code more readable.

# /etc/nginx/snippets/cors_configs.conf
#CORS Headers
add_header 'Access-Control-Allow-Origin' 'https://miapp.es' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
 
if ($request_method = 'OPTIONS') {
  add_header 'Access-Control-Max-Age' 1728000;
  add_header 'Content-Type' 'text/plain charset=UTF-8';
  add_header 'Content-Length' 0;
  return 204;
}

# /etc/nginx/sites-available/miapi.es.conf
server {
    # SERVER
    listen       80;
    server_name  api.midominio.com;

    location / {
      include 'snippets/cors_configs.conf'
       # Rest of server config file
    }
}

Including CORS in Nginx with Multiple Origins

But what if we have two or more legitimate origins that need to access an API with CORS? In other words, the miapi.es API needs to be accessed from miapp.es as well as from miapp.es:4000 and adminapp.es. The previous CORS configuration won't work for us in this case because it only allows access from miapp.es; no other origin would be accepted. But don't worry, there's a solution.

Now we face the challenge that multiple origins need to be legitimate, but the Cross-Origin Resource Sharing protocol only allows one origin in its Access-Control-Allow-Origin header. However, if this header were dynamic, we could respond differently depending on the request's origin that reaches Nginx. In other words, we can analyze the origin header that arrives at Nginx, check if it's legitimate, and dynamically respond based on the check. The good news is that this is possible in Nginx, and in fact, we're going to do it next. This will allow us to have multiple valid origins.

First, let me introduce you to the code that will validate if the origin is legitimate or not. In the following Nginx code snippet, we will define the valid and allowed origins. Remember, following our example, we should allow the domains miapp.es, miapp.es:4000, and adminapp.es. In your case, you should include the domains and subdomains you want to allow:

# /etc/nginx/snippets/cors-sites.conf
map "$http_origin" $cors {
  default '';
  "~^https?://miapp.es(:[0-9]+)?$" "$http_origin";
  "~^https?://adminapp?$" "$http_origin";
}

This snippet is very simple, although it may seem curious at first glance. Let me explain: the map in Nginx is somewhat like a switch case in a programming language. Keep in mind that the browser's origin header is translated into the variable $http_origin; Nginx does this for us.

In plain text, what we are saying is: "based on the value of $http_origin, set the variable $cors".

  • If the http_origin variable matches the regular expression ~^https?://miapp.es(:[0-9]+)?$, then cors = http_origin.
  • If the http_origin variable matches the regular expression ~^https?://adminapp?$, then $cors = $http_origin".
  • If the http_origin variable doesn't match anything, $cors = "".

In this way, if the request comes from an authorized origin, $cors will have that origin's value. Otherwise, $cors will be empty. Essentially, we are identifying and validating the origin in a single step.

Having done this, and having understood the code, we need to edit the cors_configs.conf snippet that we created in the previous section so that it prints the CORS headers or not based on the value of the $cors variable; in other words, if $cors is not empty, it prints the CORS headers. The file looks like this:

# /etc/nginx/snippets/cors_configs.conf
# Determine if it's a valid origin and set it in the $cors variable.
map "$http_origin" $cors {
  default '';
  "~^https?://miapp.es(:[0-9]+)?$" "$http_origin";
  "~^https?://adminapp?$" "$http_origin";
}

# Attach CORS headers only if it's a valid origin ($cors should not be empty)
if ($cors != "") {
  add_header 'Access-Control-Allow-Origin' "$cors" always; # <-- Variable $cors
  add_header 'Access-Control-Allow-Credentials' 'true' always;
  add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
  add_header 'Access-Control-Allow-Headers' 'Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With' always;
}

# Check if it's a preflight request and "cache" it for 20 days
if ($request_method = 'OPTIONS') {
  add_header 'Access-Control-Max-Age' 1728000;
  add_header 'Content-Type' 'text/plain charset=UTF-8';
  add_header 'Content-Length' 0;
  return 204;
}

Ready! Now Nginx will dynamically check the origin within a list of allowed origins. If it matches, it will attach the CORS headers so that the browser won't pose any issues. On the contrary, if the request's origin isn't legitimate, it won't print the headers, and the browser will block the requests.

To use this code, all you need to do is include the snippet include 'snippets/cors_configs.conf' that we saw earlier (but always at the location level). And that's it! You now have an Nginx configuration that allows secure and convenient CORS for all your authorized domains and subdomains.

Conclusion

Many times, typically due to lack of knowledge, we dislike something that is actually making our browsing safer. Undoubtedly, this is the case with Cross-Origin Resource Sharing. Many developers fear or have a certain aversion to it because they don't really know what it is and how it safeguards us (and I used to be in this group).

However, now that we have learned how to configure an Nginx server to work while respecting Cross-Origin Resource Sharing, both with a single origin and multiple origins, this protocol should no longer intimidate you, and you should feel more at ease knowing that no other origin that you authorize can make requests from a browser.

I hope I've made your life a bit easier and helped you avoid putting a wildcard in the Access-Control-Allow-Origin header ever, ever again.

Happy coding!