Opening a REST service for browser use requires CORS. Browsers have a very strict cross-domain policy that will either block the request, or just block access to the returned content. If you intend on having an open service these restrictions just get in the way. CORS is the answer, but it isn’t trivial to setup. Many of the references online are incorrect and won’t lead you to a working solution. I now have a working solution on my Redid client servers and am sharing my approach here.
Note that a completely open server is only appropriate for some services. If you actually have a social system like Facebook, or one where the user has client credentials, then an open server is not likely what you want. Of course CORS only offers a minimal level of protection. You absolutely need server-side verification. I’ll cover the security at the end.
Simple requests
The CORS standard makes a distinction between a simple request and preflight request. Just ignore the simple request; its applicability is very limited. Also, providing full preflight support covers the simple requests, should the browser make one. There is no need to do special “simple request” support.
OPTIONS request and headers
The browser first makes an OPTIONS
method request to the URL it wishes to access. The response is expected to contain a series of headers specifying the access restrictions. It doesn’t go to a special endpoint. It uses whatever URL it happens to want at the moment. After it gets the OPTIONS
response the browser makes the real request: GET
, POST
, or some other method. This response must also include the same access headers.
It’s best to return the appropriate headers on every request to your server. Every endpoint. Every method. Most frameworks make this relatively easy. Below is the code I use in Flask: it adds the headers to every response. Note my comment about failures: the client also needs access to failure results, thus it is really important that all responses have CORS headers.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@app.after_request def add_cors(resp): """ Ensure all responses have the CORS headers. This ensures any failures are also accessible by the client. """ resp.headers['Access-Control-Allow-Origin'] = flask.request.headers.get('Origin','*') resp.headers['Access-Control-Allow-Credentials'] = 'true' resp.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS, GET' resp.headers['Access-Control-Allow-Headers'] = flask.request.headers.get( 'Access-Control-Request-Headers', 'Authorization' ) # set low for debugging if app.debug: resp.headers['Access-Control-Max-Age'] = '1' return resp |
Access-Control-Allow-Origin
Many references state a value of *
should be provided here. This won’t work. All preflighted requests must include a specific origin and not a wildcard. Fortunately for you the request must also have an Origin
header. Simply copy the Origin
request header to the Access-Control-Allow-Origin
response.
If for some reason there is no Origin
header use the wildcard. This may cover a few old browsers situations with simple requests.
Access-Control-Allow-Credentials
Setting this to true
indicates that browser is allowed to send the user’s credentials to the server (it sends cookies). Not allowing this by default is the core protection against cross-site forgery of requests. This flag is also required if you wish to do HTTP authentication.
Access-Control-Allow-Methods
Only the methods listed here will be allowed for requests. If you only handle a limited number of methods you can simply list them here. The client will however also indicate which methods it wants in the Access-Control-Request-Methods
header. You can also echo that in the response.
Access-Control-Allow-Headers
By default the client is only allowed to look at a limited number of header fields in the response. If it wishes to see additional headers they must be listed here. The incoming request will have a Access-Control-Request-Headers
field that lists the headers it wants. If the OPTIONS
response do not allow access to these headers the entire request will be blocked by the browser. No request with partial header access will be performed.
The best response here is thus an echo of the incoming request. Copy the request Access-Control-Request-Headers
to the response header Access-Control-Allow-Headers
. In my code I put a fallback of Authorization
in, though in practice I’m not sure that is ever used.
Access-Control-Max-Age
Without this setting you’ll just go mad trying to debug your system. The browser caches the response of preflight requests. It does this to avoid repeated OPTIONS
requests to the same server. If you’re actively working on CORS support this becomes a problem: you make some changes and the browser just won’t see them.
The Access-Control-Max-Age
header indicates how long the browser may cache the response. I set this to 1
second for development. I want my changes to be reflected quickly while testing. For deployment I don’t return the header and just let the user-agent (browser) decide how long to cache. Firefox seems to clear this cache each session.
CORS does not protect you
That should get your running with CORS. It opens up access to your website so any request from the browser will be honoured and accessible. This is critical if you intend on offering cross-domain services.
It’s important to note that CORS doesn’t offer any kind of API security. Typical browsers honour these settings, but a non-browser agent can do whatever it wants. The only thing the browser is protecting are access credentials: your cookies. Unfortunately the cross-domain policies of browsers is so restrictive that normal and safe requests are also blocked.
This makes the CORS standard highly redundant. All of these settings give an illusion of additional safety. In actuality all that was needed was a guarantee that the Origin
header is set. So long as you receive this header your server is capable of making all security decisions on its own. There is simply no need for anything more complex.