mirror of
https://github.com/almet/notmyidea.git
synced 2025-04-28 19:42:37 +02:00
updating the cors article with some feedback
This commit is contained in:
parent
dbfe4dd98e
commit
673c563e20
3 changed files with 257 additions and 2 deletions
BIN
content/images/cors_flow.png
Normal file
BIN
content/images/cors_flow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
254
content/python/cornice-cors.rst
Normal file
254
content/python/cornice-cors.rst
Normal file
|
@ -0,0 +1,254 @@
|
|||
Implementing CORS in Cornice
|
||||
############################
|
||||
|
||||
:date: 22-01-2013
|
||||
:slug: cross-origin-in-cornice
|
||||
:status: draft
|
||||
|
||||
For security reasons, it's not possible to do cross-domain requests. In other
|
||||
words, if you have a page served from the domain `lolnet.org`, it will not be
|
||||
possible for it to get data from `notmyidea.org`.
|
||||
|
||||
Well, it's possible, using tricks and techniques like `JSONP
|
||||
<http://en.wikipedia.org/wiki/JSONP>`_, but that doesn't work all the time (see
|
||||
`the section below <#how-this-is-different-from-jsonp>`_). I remember myself
|
||||
doing some simple proxies on my domain server to be able to query other's API.
|
||||
|
||||
Hopefuly, there is a nicer way to do this, namely, "Cross Origin
|
||||
Resource-Sharing", or `CORS <http://www.w3.org/TR/cors/>`_.
|
||||
|
||||
You want an icecream? Go ask your dad first.
|
||||
============================================
|
||||
|
||||
If you want to use CORS, you need the API you're querying to support it; on the
|
||||
server side.
|
||||
|
||||
The HTTP server need to answer to the `OPTIONS` verb, and with the appropriate
|
||||
response headers.
|
||||
|
||||
`OPTIONS` is sent as what the authors of the spec call a "preflight request";
|
||||
just before doing a request to the API, the *User-Agent* (the browser most of
|
||||
the time) asks the permission to the resource, with an `OPTIONS` call.
|
||||
|
||||
The server answers, and tell what is available and what isn't:
|
||||
|
||||
.. image:: |filename|/images/cors_flow.png
|
||||
:alt: The CORS flow (from the HTML5 CORS tutorial)
|
||||
|
||||
- 1a. The User-Agent, rather than doing the call directly, asks the server, the
|
||||
API, the permission to do the request. It does so with the following headers:
|
||||
|
||||
- **Access-Control-Request-Headers**, contains the headers the User-Agent
|
||||
want to access.
|
||||
- **Access-Control-Request-Method** contains the method the User-Agent want
|
||||
to access.
|
||||
|
||||
- 1b. The API answers what is authorized:
|
||||
|
||||
- **Access-Control-Allow-Origin** the origin that's accepted. Can be `*` or
|
||||
the domain name.
|
||||
- **Access-Control-Allow-Methods** a *list* of allowed methods. This can be
|
||||
cached. Note than the request asks permission for one method and the
|
||||
server should return a list of accepted methods.
|
||||
- **Access-Allow-Headers** a list of allowed headers, for all of the
|
||||
methods, since this can be cached as well.
|
||||
|
||||
- 2. The User-Agent can do the "normal" request.
|
||||
|
||||
|
||||
So, if you want to access the `/icecream` resource, and do a PUT there, you'll
|
||||
have the following flow::
|
||||
|
||||
> OPTIONS /icecream
|
||||
> Access-Control-Request-Methods = PUT
|
||||
> Origin: notmyidea.org
|
||||
< Access-Control-Allow-Origin = notmyidea.org
|
||||
< Access-Control-Allow-Methods = PUT,GET,DELETE
|
||||
200 OK
|
||||
|
||||
You can see that we have an `Origin` Header in the request, as well as
|
||||
a `Access-Control-Request-Methods`. We're here asking if we have the right, as
|
||||
`notmyidea.org`, to do a `PUT` request on `/icecream`.
|
||||
|
||||
And the server tells us that we can do that, as well as `GET` and `DELETE`.
|
||||
|
||||
I'll not cover all the details of the CORS specification here, but bear in mind
|
||||
than with CORS, you can control what are the authorized methods, headers,
|
||||
origins, and if the client is allowed to send authentication information or
|
||||
not.
|
||||
|
||||
|
||||
A word about security
|
||||
=====================
|
||||
|
||||
CORS is not an answer for every cross-domain call you want to do, because you
|
||||
need to control the service you want to call. For instance, if you want to
|
||||
build a feed reader and access the feeds on different domains, you can be
|
||||
pretty much sure that the servers will not implement CORS, so you'll need to
|
||||
write a proxy yourself, to provide this.
|
||||
|
||||
Secondly, CORS, if misunderstood, can be unsecure, and cause some security
|
||||
problems. Because the rules apply when a client want to do a request to
|
||||
a server, you need to be extra careful about who you're authorizing.
|
||||
|
||||
A CORS uncorrectly-secured server can be accessed by a client very easily,
|
||||
bypassing the network security. For instance, if a service runs on an intranet,
|
||||
only available from behind a VPN, and accepts every cross-origin call, then
|
||||
anyone service javascript files to the browser of an user with access to this
|
||||
service could make calls there, which is probably not what you want.
|
||||
|
||||
|
||||
How this is different from JSONP?
|
||||
=================================
|
||||
|
||||
You may know the `JSONP <http://en.wikipedia.org/wiki/JSONP>`_ protocol. JSONP
|
||||
Allows to do cross origin, but for a particular use case, and do have some
|
||||
drawbacks (for instance, it's not possible to do DELETEs or PUTs with JSONP.
|
||||
|
||||
JSONP exploits the fact that's possible to get information from another domain
|
||||
when you are asking for javascript code, using the `<script>` element.
|
||||
|
||||
Exploiting the open policy for <script> elements, some pages use them to
|
||||
retrieve JavaScript code that operates on dynamically generated
|
||||
JSON-formatted data from other origins. This usage pattern is known as
|
||||
JSONP. Requests for JSONP retrieve not JSON, but arbitrary JavaScript code.
|
||||
They are evaluated by the JavaScript interpreter, not parsed by a JSON
|
||||
parser.
|
||||
|
||||
Using CORS in Cornice
|
||||
=====================
|
||||
|
||||
Okay, things are hopefully clearer by now about CORS, so let's see how we
|
||||
implemented it on the server-side.
|
||||
|
||||
Cornice is a toolkit that let you define resources in python, and takes care of
|
||||
the heavy lifting for you, so I wanted it to take care of the CORS support as
|
||||
well.
|
||||
|
||||
In Cornice, you define a service like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cornice import Service
|
||||
|
||||
foobar = Service(name="foobar", path="/foobar")
|
||||
|
||||
# and then you do something with it
|
||||
@foobar.get()
|
||||
def get_foobar(request):
|
||||
# do something with the request.
|
||||
|
||||
To add CORS support to this resource, you can go this way, with the
|
||||
`cors_origins` parameter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
foobar = Service(name='foobar', path='/foobar', cors_origins=('*',))
|
||||
|
||||
Tadam, you have enabled CORS for your service. **Be aware that here, you're
|
||||
authorizing anyone to query your server, that may not be what you want.**
|
||||
|
||||
Of course, you can specify a list of origins you trust, and you don't need
|
||||
to stick with `*`, which means "authorize everyone".
|
||||
|
||||
Headers
|
||||
-------
|
||||
|
||||
You can define the headers you want to expose for the service:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
foobar = Service(name='foobar', path='/foobar', cors_origins=('*',))
|
||||
|
||||
@foobar.get(cors_headers=('X-My-Header', 'Content-Type'))
|
||||
def get_foobars_please(request):
|
||||
return "some foobar for you"
|
||||
|
||||
I've done some testing and it wasn't working on Chrome because I wasn't
|
||||
handling the headers the right way (The missing one was `Content-Type`, that
|
||||
chrome was asking for). With my first version of the implementation, I needed
|
||||
the service implementers to explicitely list all the headers that should be
|
||||
exposed. And of course this is not what we want.
|
||||
|
||||
So I introduced an `expose_all_headers` flag, which is set to `True` by
|
||||
default, if the service supports CORS.
|
||||
|
||||
Cookies / Credentials
|
||||
---------------------
|
||||
|
||||
By default, the requests you do to your API endpoint don't include the
|
||||
credential information, ,for security reasons. If you really want to do that,
|
||||
you need to enable it using the `cors_credentials` parameter. You can activate
|
||||
this one on a per-service basis, or on a per-method basis.
|
||||
|
||||
Caching
|
||||
-------
|
||||
|
||||
When you do a preflight request, the information returned by the server can be
|
||||
cached by the User-Agent so that it's not redone before each actual call.
|
||||
|
||||
The caching period is defined by the server, using the `Access-Control-Max-Age`
|
||||
header. You can configure this timing using the `cors_max_age` parameter.
|
||||
|
||||
Simplifying the API
|
||||
-------------------
|
||||
|
||||
We have cors_headers, cors_enabled, cors_origins, cors_credentials,
|
||||
cors_max_age, cors_expose_all_headers … It start to be a fair number of
|
||||
parameters. If you want to have a specific CORS-policy for your services, that
|
||||
can be a bit tedious to pass these to your services all the time.
|
||||
|
||||
I introduced another way to pass the CORS policy, so you can do something like
|
||||
that:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
policy = dict(enabled=False,
|
||||
headers=('X-My-Header', 'Content-Type'),
|
||||
origins=('*.notmyidea.org'),
|
||||
credentials=True,
|
||||
max_age=42)
|
||||
|
||||
foobar = Service(name='foobar', path='/foobar', cors_policy=policy)
|
||||
|
||||
Comparison with other implementations
|
||||
=====================================
|
||||
|
||||
I was curious enough to have a look at the other implementations of CORS for
|
||||
django, for instance, and I found `a gist about it
|
||||
<https://gist.github.com/426829.js>`_.
|
||||
|
||||
Basically, this adds a middleware that adds the "rights" headers to the answer,
|
||||
depending on the request.
|
||||
|
||||
While this approach works, It's not implementing the specification completely:
|
||||
you need to add support for all the resources at once.
|
||||
|
||||
We can think about a nice way to implement this, with definition of what's
|
||||
supposed to be exposed via CORS and what shouldn't, directly in your settings,
|
||||
but CORS support should in my opinion be handled at the service definition level,
|
||||
and not anywhere else (for instance in the settings), appart for the list of
|
||||
authorized hosts, because you don't know exactly what's going on when you look
|
||||
at the definition of the service.
|
||||
|
||||
Resources
|
||||
=========
|
||||
|
||||
There is a number of good resources that can be useful to you if you want to
|
||||
either understand how CORS work, or if you want to implement it yourself.
|
||||
|
||||
- http://enable-cors.org/ is useful to get started when you don't know anything
|
||||
about CORS.
|
||||
- There is a W3C wiki page containing information that may be useful about
|
||||
clients, common pitfalls etc: http://www.w3.org/wiki/CORS_Enabled
|
||||
- *HTML5 rocks* has a tutorial explaining how to implement CORS, with `a nice
|
||||
section about the server-side
|
||||
<http://www.html5rocks.com/en/tutorials/cors/#toc-adding-cors-support-to-the-server>`_.
|
||||
- Be sure to have a look at the `clients support-matrix for this feature
|
||||
<http://caniuse.com/#search=cors>`_.
|
||||
- About security, `check out this page
|
||||
<https://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity>`_
|
||||
|
||||
Of course, the W3C specification is the best resource to rely on. This
|
||||
specification isn't hard to read, so you may want to go through it. Especially
|
||||
the `"resource processing model" section <http://www.w3.org/TR/cors/#resource-processing-model>`_
|
|
@ -1,7 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
PATH = "content"
|
||||
AUTHOR = u'Alexis Métaireau'
|
||||
SITENAME = u"Alexis' log"
|
||||
AUTHOR = 'Alexis Métaireau'
|
||||
SITENAME = "Alexis' log"
|
||||
THEME = "theme"
|
||||
CSS_FILE = "wide.css"
|
||||
|
||||
|
|
Loading…
Reference in a new issue