Like many of my colleagues I haven't ever felt like I really understood what Cross Origin Resource Sharing (CORS) policies accomplish. I feel like when I try to learn more about it, I understand it even less.
CORS security policies are like the worlds worst child safety lock. Whenever I have looked at information behind it, the explanations are usually very good at explaining what the moving parts are, but rarely give a good explanation for why CORS policies exist. For more background look at these CORS explanations: MDN, Codecademy, Port Swigger, Auth0
There is so much content to look at, but really for most developers this stuff should be abstracted so you don't have to think about it. I doubt very many of us could give a comprehensive breakdown of what a browser request lifecycle looks like. Really though, you can get a long way without deeply understanding this.
As far as I understand it, CORS security defaults on the browser default to the most restrictive rules, any outbound request from content on a domain that isn't making a request to that same domain will be blocked. This is a good thing. Content in the DOM is inherently mutable, so its possible that bad things could happen after your browser renders your HTML/CSS/Javascript.
Your browser is essentially telling you "Hey, if you want to make this request you better put in the work of lying about where its coming from on your server. Get out of here with your funny business!" Servers responding to requests could do work to maintain whitelists for multiple domains, but whitelists are hard to secure and backend developers are justifiably hesitant to make changes to stuff like that.
At Meshify, we've put a fair amount of resources into using Dockerized NGINX servers in tandem with Create-React-App for most of our applications. As a result of this work we've done we are able to:
brew install mkcert
mkdir templates
in your project's root directorymkdir templates/localcerts
mkdir templates/nginx
IMPORTANT: That last line build is whatever the name of your build directory is, copy paste errors can trip you up!
Throughout these examples youll see text like <YOUR_BACKEND_SERVER_URL_WITH_API>
. I'm expecting you to substitute these with your own endpoints and text. Trailing slashes can be important. If you're running your app and getting 404's, its possible your API slashes are mismatched with your NGINX config. Be careful!
yarn mkcert
in your root directoryyarn build:docker
yarn start:docker
docker-compose up
In our case, we forward requests matching the string 'periscope' to a standalone service that we made to handle some business logic. The standalone service could also be lambdas or some other endpoint that we own. In this instance we get to use the cookie from an authenticated user in that standalone service to make another request to the API to ensure that user has permissions to read what they are accessing.
My brilliant coworker Danil did most of the heavy lifting getting this setup working smoothly with NGINX. Kubernetes works especially well with this setup, COMMAND and ENVIRONMENT exists in Kubernetes configuration the same way it does here, so there are few changes necessary.
I encourage you to comment here if you have any trouble getting this running and wish you luck in breaking out of the cage that your browser puts you in!
FROM nginx:stable-alpine
COPY templates/nginx /usr/share/nginx/templates
COPY templates/localcerts /usr/share/nginx/certs
WORKDIR /usr/share/nginx/html
COPY build /usr/share/nginx/html
scripts: {
...
"build:docker": "yarn build && docker build -t <WHATEVER_YOU_WANT_TO_NAME_YOUR_CONTAINER> .",
"mkcert": "mkcert -key-file ./templates/localcerts/key.pem -cert-file ./templates/localcerts/cert.pem admin-react.local *.admin-react.local",
"start:docker": "cross-env REACT_APP_ENV=development PORT=3009 DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start"
...
}
# Added proxy_host and upstream_addr for better view of proxied requests
log_format extended '${D}remote_addr - ${D}remote_user [${D}time_local] ${D}proxy_host - ${D}upstream_addr '
'"${D}request" ${D}status ${D}body_bytes_sent '
'"${D}http_referer" "${D}http_user_agent"'
'rt=${D}request_time uct="${D}upstream_connect_time" uht="${D}upstream_header_time" urt="${D}upstream_response_time"';
access_log /var/log/nginx/access.log extended;
upstream api_server {
server ${BACKEND_SERVER};
}
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /usr/share/nginx/certs/cert.pem;
ssl_certificate_key /usr/share/nginx/certs/key.pem;
server_name admin-react.local;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# Create React App Specific
location /sockjs-node/ {
proxy_pass ${FRONTEND_URL}/sockjs-node/;
proxy_http_version 1.1;
proxy_set_header Upgrade ${D}http_upgrade;
proxy_set_header Connection "upgrade";
}
location /api/<YOUR_WEBSOCKET_ENDPOINT> {
proxy_http_version 1.1;
proxy_set_header Upgrade ${D}http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass ${BACKEND_URL}stream;
}
# It might need to change depending on what your api url looks like
location /api/ {
add_header 'Host' api_server always;
add_header 'Access-Control-Allow-Origin' "${D}http_origin" 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;
# required to be able to read Authorization header in frontend
if (${D}request_method = 'OPTIONS') {
# Tell client that this pre-flight info is valid for 20 days
add_header 'Access-Control-Allow-Origin' "${D}http_origin" always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_pass ${BACKEND_URL};
}
location / {
proxy_pass ${FRONTEND_URL};
}
}
version: "2.4"
services:
<WHATEVER YOU WANT YOUR SERVICE NAME TO BE>:
image: <YOUR DOCKER IMAGE NAME>:latest
ports:
- "3006:443"
environment:
- D=$$
- FRONTEND_URL=http://host.docker.internal:3009/
- BACKEND_SERVER=<YOUR_BACKEND_SERVER_URL_WITHOUT_API>
- BACKEND_URL=<YOUR_BACKEND_SERVER_URL_WITH_API>
command: /bin/sh -c "envsubst < /usr/share/nginx/templates/localhost.conf > /etc/nginx/conf.d/localhost.conf && exec nginx -g 'daemon off;'"