-
-
Save algal/5480916 to your computer and use it in GitHub Desktop.
# | |
# A CORS (Cross-Origin Resouce Sharing) config for nginx | |
# | |
# == Purpose | |
# | |
# This nginx configuration enables CORS requests in the following way: | |
# - enables CORS just for origins on a whitelist specified by a regular expression | |
# - CORS preflight request (OPTIONS) are responded immediately | |
# - Access-Control-Allow-Credentials=true for GET and POST requests | |
# - Access-Control-Max-Age=20days, to minimize repetitive OPTIONS requests | |
# - various superluous settings to accommodate nonconformant browsers | |
# | |
# == Comment on echoing Access-Control-Allow-Origin | |
# | |
# How do you allow CORS requests only from certain domains? The last | |
# published W3C candidate recommendation states that the | |
# Access-Control-Allow-Origin header can include a list of origins. | |
# (See: http://www.w3.org/TR/2013/CR-cors-20130129/#access-control-allow-origin-response-header ) | |
# However, browsers do not support this well and it likely will be | |
# dropped from the spec (see, http://www.rfc-editor.org/errata_search.php?rfc=6454&eid=3249 ). | |
# | |
# The usual workaround is for the server to keep a whitelist of | |
# acceptable origins on the server (as a regular expression), match | |
# the request's Origin header against the list, and echo it back | |
# | |
# (Yes you can use '*' to accept all origins but this is too open and | |
# prevents using 'Access-Control-Allow-Credentials: true', which is | |
# needed for HTTP Basic Access authentication.) | |
# | |
# == Comment on spec | |
# | |
# Comments below are all based on my reading of the CORS spec as of | |
# 2013-Jan-29 ( http://www.w3.org/TR/2013/CR-cors-20130129/ ), the | |
# XMLHttpRequest spec ( | |
# http://www.w3.org/TR/2012/WD-XMLHttpRequest-20121206/ ), and | |
# experimentation with latest versions of Firefox, Chrome, Safari at | |
# that point in time. | |
# | |
# == Changelog | |
# | |
# based on https://gist.github.com/alexjs/4165271 | |
# | |
location / { | |
# if the request included an Origin: header with an origin on the whitelist, | |
# then it is some kind of CORS request. | |
# specifically, this example allow CORS requests from | |
# scheme : http or https | |
# authority : any authority ending in "mckinsey.com" | |
# port : nothing, or :<any_number> | |
if ($http_origin ~* (https?://.*\.mckinsey\.com(:[0-9]+)?)) { | |
set $cors "true"; | |
} | |
# Nginx doesn't support nested If statements, so we use string | |
# concatenation to create a flag for compound conditions | |
# OPTIONS indicates a CORS pre-flight request | |
if ($request_method = 'OPTIONS') { | |
set $cors "${cors}options"; | |
} | |
# non-OPTIONS indicates a normal CORS request | |
if ($request_method = 'GET') { | |
set $cors "${cors}get"; | |
} | |
if ($request_method = 'POST') { | |
set $cors "${cors}post"; | |
} | |
# if it's a GET or POST, set the standard CORS responses header | |
if ($cors = "trueget") { | |
# Tells the browser this origin may make cross-origin requests | |
# (Here, we echo the requesting origin, which matched the whitelist.) | |
add_header 'Access-Control-Allow-Origin' "$http_origin"; | |
# Tells the browser it may show the response, when XmlHttpRequest.withCredentials=true. | |
add_header 'Access-Control-Allow-Credentials' 'true'; | |
# # Tell the browser which response headers the JS can see, besides the "simple response headers" | |
# add_header 'Access-Control-Expose-Headers' 'myresponseheader'; | |
} | |
if ($cors = "truepost") { | |
# Tells the browser this origin may make cross-origin requests | |
# (Here, we echo the requesting origin, which matched the whitelist.) | |
add_header 'Access-Control-Allow-Origin' "$http_origin"; | |
# Tells the browser it may show the response, when XmlHttpRequest.withCredentials=true. | |
add_header 'Access-Control-Allow-Credentials' 'true'; | |
# # Tell the browser which response headers the JS can see | |
# add_header 'Access-Control-Expose-Headers' 'myresponseheader'; | |
} | |
# if it's OPTIONS, for a CORS preflight request, then respond immediately with no response body | |
if ($cors = "trueoptions") { | |
# Tells the browser this origin may make cross-origin requests | |
# (Here, we echo the requesting origin, which matched the whitelist.) | |
add_header 'Access-Control-Allow-Origin' "$http_origin"; | |
# in a preflight response, tells browser the subsequent actual request can include user credentials (e.g., cookies) | |
add_header 'Access-Control-Allow-Credentials' 'true'; | |
# | |
# Return special preflight info | |
# | |
# Tell browser to cache this pre-flight info for 20 days | |
add_header 'Access-Control-Max-Age' 1728000; | |
# Tell browser we respond to GET,POST,OPTIONS in normal CORS requests. | |
# | |
# Not officially needed but still included to help non-conforming browsers. | |
# | |
# OPTIONS should not be needed here, since the field is used | |
# to indicate methods allowed for "actual request" not the | |
# preflight request. | |
# | |
# GET,POST also should not be needed, since the "simple | |
# methods" GET,POST,HEAD are included by default. | |
# | |
# We should only need this header for non-simple requests | |
# methods (e.g., DELETE), or custom request methods (e.g., XMODIFY) | |
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; | |
# Tell browser we accept these headers in the actual request | |
# | |
# A dynamic, wide-open config would just echo back all the headers | |
# listed in the preflight request's | |
# Access-Control-Request-Headers. | |
# | |
# A dynamic, restrictive config, would just echo back the | |
# subset of Access-Control-Request-Headers headers which are | |
# allowed for this resource. | |
# | |
# This static, fairly open config just returns a hardcoded set of | |
# headers that covers many cases, including some headers that | |
# are officially unnecessary but actually needed to support | |
# non-conforming browsers | |
# | |
# Comment on some particular headers below: | |
# | |
# Authorization -- practically and officially needed to support | |
# requests using HTTP Basic Access authentication. Browser JS | |
# can use HTTP BA authentication with an XmlHttpRequest object | |
# req by calling | |
# | |
# req.withCredentials=true, and | |
# req.setRequestHeader('Authorization','Basic ' + window.btoa(theusername + ':' + thepassword)) | |
# | |
# Counterintuitively, the username and password fields on | |
# XmlHttpRequest#open cannot be used to set the authorization | |
# field automatically for CORS requests. | |
# | |
# Content-Type -- this is a "simple header" only when it's | |
# value is either application/x-www-form-urlencoded, | |
# multipart/form-data, or text/plain; and in that case it does | |
# not officially need to be included. But, if your browser | |
# code sets the content type as application/json, for example, | |
# then that makes the header non-simple, and then your server | |
# must declare that it allows the Content-Type header. | |
# | |
# Accept,Accept-Language,Content-Language -- these are the | |
# "simple headers" and they are officially never | |
# required. Practically, possibly required. | |
# | |
# Origin -- logically, should not need to be explicitly | |
# required, since it's implicitly required by all of | |
# CORS. officially, it is unclear if it is required or | |
# forbidden! practically, probably required by existing | |
# browsers (Gecko does not request it but WebKit does, so | |
# WebKit might choke if it's not returned back). | |
# | |
# User-Agent,DNT -- officially, should not be required, as | |
# they cannot be set as "author request headers". practically, | |
# may be required. | |
# | |
# My Comment: | |
# | |
# The specs are contradictory, or else just confusing to me, | |
# in how they describe certain headers as required by CORS but | |
# forbidden by XmlHttpRequest. The CORS spec says the browser | |
# is supposed to set Access-Control-Request-Headers to include | |
# only "author request headers" (section 7.1.5). And then the | |
# server is supposed to use Access-Control-Allow-Headers to | |
# echo back the subset of those which is allowed, telling the | |
# browser that it should not continue and perform the actual | |
# request if it includes additional headers (section 7.1.5, | |
# step 8). So this implies the browser client code must take | |
# care to include all necessary headers as author request | |
# headers. | |
# | |
# However, the spec for XmlHttpRequest#setRequestHeader | |
# (section 4.6.2) provides a long list of headers which the | |
# the browser client code is forbidden to set, including for | |
# instance Origin, DNT (do not track), User-Agent, etc.. This | |
# is understandable: these are all headers that we want the | |
# browser itself to control, so that malicious browser client | |
# code cannot spoof them and for instance pretend to be from a | |
# different origin, etc.. | |
# | |
# But if XmlHttpRequest forbids the browser client code from | |
# setting these (as per the XmlHttpRequest spec), then they | |
# are not author request headers. And if they are not author | |
# request headers, then the browser should not include them in | |
# the preflight request's Access-Control-Request-Headers. And | |
# if they are not included in Access-Control-Request-Headers, | |
# then they should not be echoed by | |
# Access-Control-Allow-Headers. And if they are not echoed by | |
# Access-Control-Allow-Headers, then the browser should not | |
# continue and execute actual request. So this seems to imply | |
# that the CORS and XmlHttpRequest specs forbid certain | |
# widely-used fields in CORS requests, including the Origin | |
# field, which they also require for CORS requests. | |
# | |
# The bottom line: it seems there are headers needed for the | |
# web and CORS to work, which at the moment you should | |
# hard-code into Access-Control-Allow-Headers, although | |
# official specs imply this should not be necessary. | |
# | |
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since'; | |
# build entire response to the preflight request | |
# no body in this response | |
add_header 'Content-Length' 0; | |
# (should not be necessary, but included for non-conforming browsers) | |
add_header 'Content-Type' 'text/plain charset=UTF-8'; | |
# indicate successful return with no content | |
return 204; | |
} | |
} |
Thanks for the comment. I can't check it right now, but that sounds 100% correct.
Use map
instead http://serverfault.com/questions/674900/nginx-if-statement-inside-location-returns-404
If is evil. Lots of configuration, such as those using try_files
will not work properly if you use if
like this. See: http://wiki.nginx.org/IfIsEvil
@thoughtless, do you have a gist that uses map
?
@thoughtless, can you elaborate how you would avoid including blank headers when using map
? Your comment doesn't provide a whole lot of insight or guidance. For example, I don't see this working very well:
# ...
map "${request_method}" $cors_method {
'~*^(get|post)$' 'getpost';
'~*^OPTIONS$' 'options';
}
map "${http_origin}" $cors_enable {
default '';
'~*^https?://[^/]+\.(example)\.com(:[0-9]+)?$' 'o';
'~*^null' '*'; # for file:// URLs
}
map "${cors_enable}-${cors_method}" $allow_origin_header {
default '';
'~^o-(getpost|options)$' "$http_origin";
'~^\*-(getpost|options)$' '*';
}
# ...
map "${cors_enable}-${cors_method}" $content_length_header {
# This should only get set with the OPTIONS method
default '';
'~^[*o]-options$' '0';
}
add_header 'Access-Control-Allow-Origin' "${allow_origin_header}"; # probably okay
# ...
add_header 'Content-Length' "${content_length_header}"; # huh?
if ($content_length_header != '') {
return 204;
}
# ...
@posita I use maps in this gist https://gist.github.com/slbmeh/6e2dbc1218a0be0d7ae2
@algal - I reworked your conf to remove as many ifs as possible by using redirects instead. The main if check on $http_origin is still required. Instead of setting variables and checking them, I rewrite to /cors/$request_method$uri, I then create 3 location blocks, /cors/GET/, /cors/POST/, /cors/OPTIONS/ to set the appropriate headers and then rewrite once more to remove the cors prefix so Nginx will fall back to whatever locations you have set up. I had to rework it as it was causing me issues with v.1.6.2 and using it with proxy_pass to a uri end point.
it seems like https://gist.github.com/slbmeh/6e2dbc1218a0be0d7ae2 not work,
but https://gist.github.com/sbuzonas/6e2dbc1218a0be0d7ae2 dose.
I noted this on the original but you probably want a
$
at the end of the regex on line 54. Without ithttps://anything.mckinsey.com.evil.com
gets through.