Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django-cors-headers and nginx config: preflight response missing CORS headers

I use django-cors-headers 3.1.1 for handling the requests and responses between my Django-backend and Javascript-frontend apps. Transport is non-secured (i.e. http, not https).

When hosted locally, everything works fine. But after deploying on the server, I stopped seeing the CORS headers.

Here are headers in development: enter image description here and in production: enter image description here

Error message:

Access to XMLHttpRequest at 'http://[HOST_IP]/api/assets/' from origin 'http://my_custom_domain.eu' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

My nginx configurations looks as follows:

server {
    listen 80;
    server_name [HOST_IP];

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/ubuntu/[path_to_app]/app.sock;

        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, PUT, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Max-Age' 86400;

        if ($request_method = 'OPTIONS') {
            add_header 'Content-Type' 'text/html; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        if ($request_method = 'PUT') {
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        }
        if ($request_method = 'GET') {
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        }
    }

    location /static/ {
        autoindex on;
        alias /home/ubuntu/[path_to_app]/site/static/;
    }
}

The django-cors-headers’ settings are now identical in development and in production:

INSTALLED_APPS = (
    …
    "corsheaders",
    …
)
MIDDLEWARE = [
    …
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
    …
]
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_METHODS = ['DELETE','GET','OPTIONS','PATCH','POST','PUT']

On the client side I tried to add ‘'Access-Control-Request-Method’: ‘PUT’ header, but this was refused by the browser. There’s noting unusual in the client call:

  axios({
    method: 'put',
    url: `${this.backendUrl}/api/assets/`,
    data: formData,
    headers: {
      'Content-Type': 'application/octet-stream',
    }
  })

Also, I’m trying to host on the Amazon AWS EC2 for the first time, so perhaps there is some required AWS configuration I am not aware of. For example, is it necessary to enable CORS using the API Gateway? The documentation does not say so (‘If you are using the API Gateway Import API, you can set up CORS support using an OpenAPI file’).

The frontend application is hosted on a S3 bucket with the following CORS policy:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>GET</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

What am I missing here? Is there some needed server-side (nginx especially) configuration?

Some other solutions I’ve tried:

I have doubts, whether the request/response origin is correct (e.g. APPEND_SLASH variable). But if this is the case, shouldn’t an error be raised when hosted locally?

I also tried setting proxy headers as in this question, but without knowing nginx very well this was doomed to fail.

like image 319
Pawel Kam Avatar asked Nov 07 '22 12:11

Pawel Kam


1 Answers

I managed to solve this issue by changing 3 things:

AWS

I noticed that AWS documentation states:

CORS is already enabled for the Amazon EC2 API, and is ready for you to use. You do not need to perform any additional configuration steps to start using this feature. There is no change to the way that you make calls to the Amazon EC2 API; they must still be signed with valid AWS credentials to ensure that AWS can authenticate the requestor. […]

This is usually handled by AWS SDK or CLI, but in my case I used none of them, so I had to add AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. In my case I simply used aws4 library:

  axios(aws4.sign({
    accessKeyId: this.awsAccessKey,
    secretAccessKey: this.awsSecretAccessKey,
    method: 'put',
    url: `${this.backendUrl}/api/assets/`,
    data: formData,
    body: JSON.stringify(formData),
    service: 'ec2',
    region: 'eu-central-1',
    path: '/',
    headers: {
      'Content-Type': 'application/octet-stream'
    }
  }))

I’ve seen plenty of examples how to add AWS Signature v.4 without any additional dependency, though.

NGINX

In nginx configuration I placed all add_headers statements into conditional code-blocks. Idea came from this post.

server {
    listen 80;
    server_name [HOST_IP];

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/ubuntu/[path_to_app]/app.sock;


        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, PUT, OPTIONS, POST, DELETE';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Amz-Date';
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Content-Type' 'text/html; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        if ($request_method = 'PUT') {
            add_header 'Access-Control-Allow-Methods' 'GET, PUT, OPTIONS, POST, DELETE';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Amz-Date';
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        }
        if ($request_method = 'GET') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, PUT, OPTIONS, POST, DELETE';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Amz-Date';
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        }
    }

    location /static/ {
        autoindex on;
        alias /home/ubuntu/analizy/be/site/static/;
    }
}

Django-cors-header

Here it sufficed to add non-default headers.

from corsheaders.defaults import default_headers
CORS_ALLOW_HEADERS = list(default_headers) + [
    'X-Amz-Date',
]

Hope that this will help someone.

like image 88
Pawel Kam Avatar answered Dec 07 '22 12:12

Pawel Kam