osteel's blog Web development resources

Handling CORS with Nginx

Been here before?

You can also subscribe to the RSS or Atom feed, or follow me on Twitter.

Nginx logo

[UPDATE 2015/08/02]
As @OtaK_ pointed out, in most cases CORS should be handled directly by the app as it should return the allowed verbs by endpoint, instead of all of them being allowed by Nginx. This config should only be used for quick development, of a prototype or PoC for example, or if you are certain that the same verbs are allowed for all the endpoints (that would be the case for the assets returned by a CDN, for instance).
[/UPDATE]

With the always wider adoption of API-driven architecture, chances are you already had to deal with cross-origin resource sharing at some point.

Whilst it is possible to deal with it from the code and you will find many packages or snippets to do so, we can remove the CORS handling from our app and let the HTTP server take care of it.

The Enable CORS website contains useful resources to this end, but when I tried to use their Nginx config for my own projects it didn't quite work as expected.

The following examples are based on the Nginx server configurations generated by Homestead, but the steps won't change much even if you are not using Laravel's dev environment.

nginx-extras

First of all, Nginx's traditional add_header directive doesn't work with 4xx responses. As we still want to add custom headers to them, we need to install the ngx_headers_more module to be able to use the more_set_headers directive, which also works with 4xx responses.

While the documentation suggests to build the Nginx source with the module, if you are on a Debian distro you can actually easily install it with the nginx-extras package:

sudo apt-get install nginx-extras

The server configuration

Here is what a typical server config of a Laravel project looks like, without the CORS bit (I am voluntarily omitting the SSL part to keep the post short, but it works exactly the same):

server {
    listen 80;
    server_name example-site.com;
    root "/home/vagrant/projects/example-site/public";

    index index.html index.htm index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/example-site.com-error.log error;

    sendfile off;

    client_max_body_size 100m;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
    }

    location ~ /\.ht {
        deny all;
    }
}

Now, with the CORS handling:

server {
    listen 80;
    server_name example-site.com;
    root "/home/vagrant/projects/example-site/public";

    index index.html index.htm index.php;

    charset utf-8;

    more_set_headers 'Access-Control-Allow-Origin: $http_origin';
    more_set_headers 'Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE, HEAD';
    more_set_headers 'Access-Control-Allow-Credentials: true';
    more_set_headers 'Access-Control-Allow-Headers: Origin,Content-Type,Accept,Authorization';

    location / {
        if ($request_method = 'OPTIONS') {
            more_set_headers 'Access-Control-Allow-Origin: $http_origin';
            more_set_headers 'Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE, HEAD';
            more_set_headers 'Access-Control-Max-Age: 1728000';
            more_set_headers 'Access-Control-Allow-Credentials: true';
            more_set_headers 'Access-Control-Allow-Headers: Origin,Content-Type,Accept,Authorization';
            more_set_headers 'Content-Type: text/plain; charset=UTF-8';
            more_set_headers 'Content-Length: 0';
            return 204;
        }
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off;
    error_log  /var/log/nginx/example-site.com-error.log error;

    sendfile off;

    client_max_body_size 100m;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
    }

    location ~ /\.ht {
        deny all;
    }
}

And that is pretty much it.

All you need to do now is to reload your Nginx confs:

sudo service nginx reload

Extra considerations

Note that this allows any domain to access your app, and while this is most likely enough for local development, on a production server you might want to fine-tune this configuration to allow specific domains only (Access_Control_Allow_Origin).

More generally, all the headers' values are examples and you can modify them as you see fit.

You could also put the global and options-related snippets into separate files (in /etc/nginx/shared/, for example) and import them with the Nginx's include directive.

Enjoying the content?

You can also subscribe to the RSS or Atom feed, or follow me on Twitter.

Last updated by osteel on :: [ cors crossoriginresourcesharing nginx homestead ]

Comments