Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
nginx configuration for CORS (Cross-Origin Resource Sharing), with an origin whitelist, and HTTP Basic Access authentication allowed
#
# 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;
}
}
@Ry4an

This comment has been minimized.

Copy link

commented Aug 9, 2013

I noted this on the original but you probably want a $ at the end of the regex on line 54. Without it https://anything.mckinsey.com.evil.com gets through.

@algal

This comment has been minimized.

Copy link
Owner Author

commented Oct 20, 2013

Thanks for the comment. I can't check it right now, but that sounds 100% correct.

@thoughtless

This comment has been minimized.

Copy link

commented Mar 18, 2015

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

@wizonesolutions

This comment has been minimized.

Copy link

commented Jun 17, 2015

@thoughtless, do you have a gist that uses map?

@posita

This comment has been minimized.

Copy link

commented Aug 25, 2015

@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;
}

# ...
@sbuzonas

This comment has been minimized.

Copy link

commented Sep 8, 2015

@himyouten

This comment has been minimized.

Copy link

commented Dec 4, 2015

@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.

https://gist.github.com/himyouten/df57b21958fba9c75ea7

@up9cloud

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.