CORS Headers to Permit a Fixed Set of Domains (NGINX)

Your Javascript is hosted on a different domain, say for CDN purposes, and your application is on another domain. You’re using ES modules which need to be loaded on import and you find yourself needing to set or insert the necessary headers for Cross-Origin requests. A lot of tutorials online show you how to allow *any* origin by setting the header to ‘*’ basically, which is probably not a wise idea if your static asset domain is not explictly a CDN. A slightly safer way to go is to only set the necessary headers to permit domains you authorize only.

Here is how you do this.

Use a Cascading Map to set variables

map $http_origin $cors {
  ~*^https?:\/\/.*\.subdomain\.example\.org$ 'allowed_origin';
  ~*^https?:\/\/.*\.example\.net$ 'allowed_origin';
  default '';
}

map $request_method $preflight {
  OPTIONS $cors;
  default "plain preflight";
}

The maps above require two variables. The first variable $cors simply checks if the origin of the request is permitted to make a CORS request to our site.

Because we cannot use logical statements here as far as I can know, we need to take advantage of a second map. It is important that this second map is setting a different variable. In this case, if the request method is ‘OPTIONS’ which indicates a pre-flight request, we set a second variable $preflight to the value of the first variable which contains ‘allowed_origin’ if the request origin is from a domain we want.

This “cascade” of maps allows us to effectively express an ‘AND’ logic, meaning that the value of $preflight will only be equal to ‘allowed_origin’ if both the Origin of the request is acceptable ot us, AND the request method is ‘OPTIONS’.

Use an ‘if’ to respond accordingly

location ~* .(js|mjs)$ {
  # ... other configs
  
  set $cors_allowed_methods 'OPTIONS, HEAD, GET';

  if ($cors = "allowed_origin") {
    add_header 'Access-Control-Allow-Origin' $http_origin;
    add_header 'Access-Control-Allow-Methods' $cors_allowed_methods;
    add_header 'Access-Control-Max-Age' '3600';
  }

  if ($preflight = 'allowed_origin') {
    add_header 'Access-Control-Allow-Origin' $http_origin;
    add_header 'Access-Control-Allow-Methods' $cors_allowed_methods;
    add_header 'Access-Control-Max-Age' '3600';
    add_header 'Content-Type' 'text/plain';
    add_header 'Content-Length' '0';
    return 204;
  }
}

The ‘if’ sections within a location block above simply allow us to respond with the right headers if the request is a CORS request from an orgin we permit, and also to do the right thing if it is a pre-flight request from an origin we permit.

A complete stub configuration would look something like this:

http {
  # ... your http configs
  map $http_origin $cors {
    ~*^https?:\/\/.*\.subdomain\.example\.org$ 'allowed_origin';
    ~*^https?:\/\/.*\.example\.net$ 'allowed_origin';
    default '';
  }
  
  map $request_method $preflight {
    OPTIONS $cors;
    default "plain preflight";
  }
  
  # ... other configs
  
  server {
    # .. server configs
    
    location ~* .(js|mjs)$ {
      # ... other configs
      
      set $cors_allowed_methods 'OPTIONS, HEAD, GET';
    
      if ($cors = "allowed_origin") {
        add_header 'Access-Control-Allow-Origin' $http_origin;
        add_header 'Access-Control-Allow-Methods' $cors_allowed_methods;
        add_header 'Access-Control-Max-Age' '3600';
      }
    
      if ($preflight = 'allowed_origin') {
        add_header 'Access-Control-Allow-Origin' $http_origin;
        add_header 'Access-Control-Allow-Methods' $cors_allowed_methods;
        add_header 'Access-Control-Max-Age' '3600';
        add_header 'Content-Type' 'text/plain';
        add_header 'Content-Length' '0';
        return 204;
      }
    }
  }

}

I hope you find this useful when faced with a similar challenge.