Commit 40e3fc0e by Tom Christie

Merge pull request #709 from tomchristie/oauth

OAuth support
parents 20880232 f513db71
...@@ -14,6 +14,9 @@ env: ...@@ -14,6 +14,9 @@ env:
install: install:
- pip install $DJANGO - pip install $DJANGO
- pip install defusedxml==0.3 - pip install defusedxml==0.3
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install https://github.com/alex/django-filter/tarball/master; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install https://github.com/alex/django-filter/tarball/master; fi"
- export PYTHONPATH=. - export PYTHONPATH=.
......
...@@ -111,7 +111,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401 ...@@ -111,7 +111,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
## TokenAuthentication ## TokenAuthentication
This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients. This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting: To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
...@@ -207,6 +207,97 @@ Unauthenticated responses that are denied permission will result in an `HTTP 403 ...@@ -207,6 +207,97 @@ Unauthenticated responses that are denied permission will result in an `HTTP 403
If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details. If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details.
## OAuthAuthentication
This authentication uses [OAuth 1.0a][oauth-1.0a] authentication scheme. OAuth 1.0a provides signature validation which provides a reasonable level of security over plain non-HTTPS connections. However, it may also be considered more complicated than OAuth2, as it requires clients to sign their requests.
This authentication class depends on the optional `django-oauth-plus` and `oauth2` packages. In order to make it work you must install these packages and add `oauth_provider` to your `INSTALLED_APPS`:
INSTALLED_APPS = (
...
`oauth_provider`,
)
Don't forget to run `syncdb` once you've added the package.
python manage.py syncdb
#### Getting started with django-oauth-plus
The OAuthAuthentication class only provides token verification and signature validation for requests. It doesn't provide authorization flow for your clients. You still need to implement your own views for accessing and authorizing tokens.
The `django-oauth-plus` package provides simple foundation for classic 'three-legged' oauth flow. Please refer to [the documentation][django-oauth-plus] for more details.
## OAuth2Authentication
This authentication uses [OAuth 2.0][rfc6749] authentication scheme. OAuth2 is more simple to work with than OAuth1, and provides much better security than simple token authentication. It is an unauthenticated scheme, and requires you to use an HTTPS connection.
This authentication class depends on the optional [django-oauth2-provider][django-oauth2-provider] project. In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS`:
INSTALLED_APPS = (
...
'provider',
'provider.oauth2',
)
You must also include the following in your root `urls.py` module:
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
Note that the `namespace='oauth2'` argument is required.
Finally, sync your database.
python manage.py syncdb
python manage.py migrate
---
**Note:** If you use `OAuth2Authentication` in production you must ensure that your API is only available over `https` only.
---
#### Getting started with django-oauth2-provider
The `OAuth2Authentication` class only provides token verification for requests. It doesn't provide authorization flow for your clients.
The OAuth 2 authorization flow is taken care by the [django-oauth2-provider][django-oauth2-provider] dependency. A walkthrough is given here, but for more details you should refer to [the documentation][django-oauth2-provider-docs].
To get started:
##### 1. Create a client
You can create a client, either through the shell, or by using the Django admin.
Go to the admin panel and create a new `Provider.Client` entry. It will create the `client_id` and `client_secret` properties for you.
##### 2. Request an access token
To request an access token, submit a `POST` request to the url `/oauth2/access_token` with the following fields:
* `client_id` the client id you've just configured at the previous step.
* `client_secret` again configured at the previous step.
* `username` the username with which you want to log in.
* `password` well, that speaks for itself.
You can use the command line to test that your local configuration is working:
curl -X POST -d "client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=password&username=YOUR_USERNAME&password=YOUR_PASSWORD" http://localhost:8000/oauth2/access_token/
You should get a response that looks something like this:
{"access_token": "<your-access-token>", "scope": "read", "expires_in": 86399, "refresh_token": "<your-refresh-token>"}
##### 3. Access the API
The only thing needed to make the `OAuth2Authentication` class work is to insert the `access_token` you've received in the `Authorization` request header.
The command line to test the authentication looks like:
curl -H "Authorization: Bearer <your-access-token>" http://localhost:8000/api/?client_id=YOUR_CLIENT_ID\&client_secret=YOUR_CLIENT_SECRET
---
# Custom authentication # Custom authentication
To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise. To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.
...@@ -262,3 +353,8 @@ HTTP digest authentication is a widely implemented scheme that was intended to r ...@@ -262,3 +353,8 @@ HTTP digest authentication is a widely implemented scheme that was intended to r
[south-dependencies]: http://south.readthedocs.org/en/latest/dependencies.html [south-dependencies]: http://south.readthedocs.org/en/latest/dependencies.html
[juanriaza]: https://github.com/juanriaza [juanriaza]: https://github.com/juanriaza
[djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth [djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth
[oauth-1.0a]: http://oauth.net/core/1.0a
[django-oauth-plus]: http://code.larlet.fr/django-oauth-plus
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
[django-oauth2-provider-docs]: https://django-oauth2-provider.readthedocs.org/en/latest/
[rfc6749]: http://tools.ietf.org/html/rfc6749
...@@ -36,6 +36,10 @@ The following packages are optional: ...@@ -36,6 +36,10 @@ The following packages are optional:
* [PyYAML][yaml] (3.10+) - YAML content-type support. * [PyYAML][yaml] (3.10+) - YAML content-type support.
* [defusedxml][defusedxml] (0.3+) - XML content-type support. * [defusedxml][defusedxml] (0.3+) - XML content-type support.
* [django-filter][django-filter] (0.5.4+) - Filtering support. * [django-filter][django-filter] (0.5.4+) - Filtering support.
* [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support.
* [django-oauth2-provider][django-oauth2-provider] (0.2.3+) - OAuth 2.0 support.
**Note**: The `oauth2` python package is badly misnamed, and actually provides OAuth 1.0a support. Also note that packages required for both OAuth 1.0a, and OAuth 2.0 are not yet Python 3 compatible.
## Installation ## Installation
...@@ -180,6 +184,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -180,6 +184,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[yaml]: http://pypi.python.org/pypi/PyYAML [yaml]: http://pypi.python.org/pypi/PyYAML
[defusedxml]: https://pypi.python.org/pypi/defusedxml [defusedxml]: https://pypi.python.org/pypi/defusedxml
[django-filter]: http://pypi.python.org/pypi/django-filter [django-filter]: http://pypi.python.org/pypi/django-filter
[oauth2]: https://github.com/simplegeo/python-oauth2
[django-oauth-plus]: https://bitbucket.org/david/django-oauth-plus/wiki/Home
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[image]: img/quickstart.png [image]: img/quickstart.png
[sandbox]: http://restframework.herokuapp.com/ [sandbox]: http://restframework.herokuapp.com/
......
...@@ -110,6 +110,7 @@ The following people have helped make REST framework great. ...@@ -110,6 +110,7 @@ The following people have helped make REST framework great.
* Jonas Braun - [iekadou] * Jonas Braun - [iekadou]
* Ian Dash - [bitmonkey] * Ian Dash - [bitmonkey]
* Bouke Haarsma - [bouke] * Bouke Haarsma - [bouke]
* Pierre Dulac - [dulaccc]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
...@@ -254,4 +255,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. ...@@ -254,4 +255,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[iekadou]: https://github.com/iekadou [iekadou]: https://github.com/iekadou
[bitmonkey]: https://github.com/bitmonkey [bitmonkey]: https://github.com/bitmonkey
[bouke]: https://github.com/bouke [bouke]: https://github.com/bouke
[dulaccc]: https://github.com/dulaccc
...@@ -2,3 +2,6 @@ markdown>=2.1.0 ...@@ -2,3 +2,6 @@ markdown>=2.1.0
PyYAML>=3.10 PyYAML>=3.10
defusedxml>=0.3 defusedxml>=0.3
django-filter>=0.5.4 django-filter>=0.5.4
django-oauth-plus>=2.0
oauth2>=1.5.211
django-oauth2-provider>=0.2.3
...@@ -426,3 +426,34 @@ try: ...@@ -426,3 +426,34 @@ try:
import defusedxml.ElementTree as etree import defusedxml.ElementTree as etree
except ImportError: except ImportError:
etree = None etree = None
# OAuth is optional
try:
# Note: The `oauth2` package actually provides oauth1.0a support. Urg.
import oauth2 as oauth
except ImportError:
oauth = None
# OAuth is optional
try:
import oauth_provider
from oauth_provider.store import store as oauth_provider_store
except ImportError:
oauth_provider = None
oauth_provider_store = None
# OAuth 2 support is optional
try:
import provider.oauth2 as oauth2_provider
from provider.oauth2 import backends as oauth2_provider_backends
from provider.oauth2 import models as oauth2_provider_models
from provider.oauth2 import forms as oauth2_provider_forms
from provider import scope as oauth2_provider_scope
from provider import constants as oauth2_constants
except ImportError:
oauth2_provider = None
oauth2_provider_backends = None
oauth2_provider_models = None
oauth2_provider_forms = None
oauth2_provider_scope = None
oauth2_constants = None
...@@ -7,6 +7,8 @@ import warnings ...@@ -7,6 +7,8 @@ import warnings
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
from rest_framework.compat import oauth2_provider_scope, oauth2_constants
class BasePermission(object): class BasePermission(object):
""" """
...@@ -132,3 +134,26 @@ class DjangoModelPermissions(BasePermission): ...@@ -132,3 +134,26 @@ class DjangoModelPermissions(BasePermission):
request.user.has_perms(perms)): request.user.has_perms(perms)):
return True return True
return False return False
class TokenHasReadWriteScope(BasePermission):
"""
The request is authenticated as a user and the token used has the right scope
"""
def has_permission(self, request, view):
token = request.auth
read_only = request.method in SAFE_METHODS
if not token:
return False
if hasattr(token, 'resource'): # OAuth 1
return read_only or not request.auth.resource.is_readonly
elif hasattr(token, 'scope'): # OAuth 2
required = oauth2_constants.READ if read_only else oauth2_constants.WRITE
return oauth2_provider_scope.check(required, request.auth.scope)
else:
assert False, ('TokenHasReadWriteScope requires either the'
'`OAuthAuthentication` or `OAuth2Authentication` authentication '
'class to be used.')
...@@ -97,9 +97,30 @@ INSTALLED_APPS = ( ...@@ -97,9 +97,30 @@ INSTALLED_APPS = (
# 'django.contrib.admindocs', # 'django.contrib.admindocs',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'rest_framework.tests' 'rest_framework.tests',
) )
# OAuth is optional and won't work if there is no oauth_provider & oauth2
try:
import oauth_provider
import oauth2
except ImportError:
pass
else:
INSTALLED_APPS += (
'oauth_provider',
)
try:
import provider
except ImportError:
pass
else:
INSTALLED_APPS += (
'provider',
'provider.oauth2',
)
STATIC_URL = '/static/' STATIC_URL = '/static/'
PASSWORD_HASHERS = ( PASSWORD_HASHERS = (
......
...@@ -8,46 +8,65 @@ commands = {envpython} rest_framework/runtests/runtests.py ...@@ -8,46 +8,65 @@ commands = {envpython} rest_framework/runtests/runtests.py
[testenv:py3.3-django1.5] [testenv:py3.3-django1.5]
basepython = python3.3 basepython = python3.3
deps = django==1.5 deps = django==1.5
https://github.com/alex/django-filter/archive/master.tar.gz -egit+git://github.com/alex/django-filter.git#egg=django_filter
defusedxml==0.3 defusedxml==0.3
[testenv:py3.2-django1.5] [testenv:py3.2-django1.5]
basepython = python3.2 basepython = python3.2
deps = django==1.5 deps = django==1.5
https://github.com/alex/django-filter/archive/master.tar.gz -egit+git://github.com/alex/django-filter.git#egg=django_filter
defusedxml==0.3 defusedxml==0.3
[testenv:py2.7-django1.5] [testenv:py2.7-django1.5]
basepython = python2.7 basepython = python2.7
deps = django==1.5 deps = django==1.5
django-filter==0.5.4 django-filter==0.5.4
defusedxml==0.3
django-oauth-plus==2.0
oauth2==1.5.211
django-oauth2-provider==0.2.3
[testenv:py2.6-django1.5] [testenv:py2.6-django1.5]
basepython = python2.6 basepython = python2.6
deps = django==1.5 deps = django==1.5
django-filter==0.5.4 django-filter==0.5.4
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.0
oauth2==1.5.211
django-oauth2-provider==0.2.3
[testenv:py2.7-django1.4] [testenv:py2.7-django1.4]
basepython = python2.7 basepython = python2.7
deps = django==1.4.3 deps = django==1.4.3
django-filter==0.5.4 django-filter==0.5.4
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.0
oauth2==1.5.211
django-oauth2-provider==0.2.3
[testenv:py2.6-django1.4] [testenv:py2.6-django1.4]
basepython = python2.6 basepython = python2.6
deps = django==1.4.3 deps = django==1.4.3
django-filter==0.5.4 django-filter==0.5.4
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.0
oauth2==1.5.211
django-oauth2-provider==0.2.3
[testenv:py2.7-django1.3] [testenv:py2.7-django1.3]
basepython = python2.7 basepython = python2.7
deps = django==1.3.5 deps = django==1.3.5
django-filter==0.5.4 django-filter==0.5.4
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.0
oauth2==1.5.211
django-oauth2-provider==0.2.3
[testenv:py2.6-django1.3] [testenv:py2.6-django1.3]
basepython = python2.6 basepython = python2.6
deps = django==1.3.5 deps = django==1.3.5
django-filter==0.5.4 django-filter==0.5.4
defusedxml==0.3 defusedxml==0.3
django-oauth-plus==2.0
oauth2==1.5.211
django-oauth2-provider==0.2.3
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment