mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-27 08:29:59 +03:00
Merge 5a56f92abf
into 5fffaf89e2
This commit is contained in:
commit
bea6882112
|
@ -14,6 +14,7 @@ env:
|
|||
install:
|
||||
- pip install $DJANGO
|
||||
- pip install defusedxml==0.3
|
||||
- "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 https://github.com/alex/django-filter/tarball/master; fi"
|
||||
- export PYTHONPATH=.
|
||||
|
|
|
@ -207,6 +207,80 @@ 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.
|
||||
|
||||
## OAuth2Authentication
|
||||
|
||||
---
|
||||
|
||||
** Note:** This isn't available for Python 3, because the module [`django-oauth2-provider`][django-oauth2-provider] is not Python 3 ready.
|
||||
|
||||
---
|
||||
|
||||
This authentication uses [OAuth 2.0][rfc6749] authentication scheme. It 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',
|
||||
)
|
||||
|
||||
And include the urls needed in your root `urls.py` file to be able to begin the *oauth 2 dance* :
|
||||
|
||||
url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')),
|
||||
|
||||
---
|
||||
|
||||
** Note:** The *namespace* argument is required !
|
||||
|
||||
---
|
||||
|
||||
Finally, sync your database with those two new django apps.
|
||||
|
||||
$ python manage.py syncdb
|
||||
$ python manage.py migrate
|
||||
|
||||
`OAuth2Authentication` class provides only token verification for requests. The *oauth 2 dance* is taken care by the [`django-oaut2-provider`][django-oauth2-provider] dependency. The official [documentation][django-oauth2-provider--doc] is being [rewritten][django-oauth2-provider--rewritten-doc].
|
||||
|
||||
The Good news is, here is a minimal "How to start" because **OAuth 2** is dramatically simpler than **OAuth 1**, so no more headache with signature, cryptography on client side, and other complex things.
|
||||
|
||||
### How to start with *django-oauth2-provider* ?
|
||||
|
||||
#### Create a client in the django-admin panel
|
||||
|
||||
Go to the admin panel and create a new `Provider.Client` entry. It will create the `client_id` and `client_secret` properties for you.
|
||||
|
||||
#### Request an access token
|
||||
|
||||
Your client interface – I mean by that your iOS code, HTML code, or whatever else language – just have to submit a `POST` request at 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.
|
||||
|
||||
---
|
||||
|
||||
**Note:** Remember that you should use HTTPS in production.
|
||||
|
||||
---
|
||||
|
||||
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/
|
||||
|
||||
Here is the response you should get :
|
||||
|
||||
{"access_token": "<your-access-token>", "scope": "read", "expires_in": 86399, "refresh_token": "<your-refresh-token>"}
|
||||
|
||||
#### 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` api 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
|
||||
|
||||
And it will work like a charm.
|
||||
|
||||
# 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.
|
||||
|
@ -262,3 +336,7 @@ HTTP digest authentication is a widely implemented scheme that was intended to r
|
|||
[south-dependencies]: http://south.readthedocs.org/en/latest/dependencies.html
|
||||
[juanriaza]: https://github.com/juanriaza
|
||||
[djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth
|
||||
[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
|
||||
[django-oauth2-provider--doc]: https://django-oauth2-provider.readthedocs.org/en/latest/
|
||||
[django-oauth2-provider--rewritten-doc]: http://django-oauth2-provider-dulaccc.readthedocs.org/en/latest/
|
||||
[rfc6749]: http://tools.ietf.org/html/rfc6749
|
||||
|
|
|
@ -2,3 +2,4 @@ markdown>=2.1.0
|
|||
PyYAML>=3.10
|
||||
defusedxml>=0.3
|
||||
django-filter>=0.5.4
|
||||
django-oauth2-provider>=0.2.3
|
|
@ -6,6 +6,7 @@ from django.contrib.auth import authenticate
|
|||
from django.utils.encoding import DjangoUnicodeDecodeError
|
||||
from rest_framework import exceptions, HTTP_HEADER_ENCODING
|
||||
from rest_framework.compat import CsrfViewMiddleware
|
||||
from rest_framework.compat import oauth2_provider
|
||||
from rest_framework.authtoken.models import Token
|
||||
import base64
|
||||
|
||||
|
@ -155,4 +156,78 @@ class TokenAuthentication(BaseAuthentication):
|
|||
return 'Token'
|
||||
|
||||
|
||||
# TODO: OAuthAuthentication
|
||||
class OAuth2Authentication(BaseAuthentication):
|
||||
"""
|
||||
OAuth 2 authentication backend using `django-oauth2-provider`
|
||||
"""
|
||||
require_active = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(OAuth2Authentication, self).__init__(**kwargs)
|
||||
if oauth2_provider is None:
|
||||
raise ImproperlyConfigured("The 'django-oauth2-provider' package could not be imported. It is required for use with the 'OAuth2Authentication' class.")
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
The Bearer type is the only finalized type
|
||||
|
||||
Read the spec for more details
|
||||
http://tools.ietf.org/html/rfc6749#section-7.1
|
||||
"""
|
||||
auth = request.META.get('HTTP_AUTHORIZATION', '').split()
|
||||
if not auth or auth[0].lower() != "bearer":
|
||||
raise exceptions.AuthenticationFailed('Invalid Authorization token type')
|
||||
|
||||
if len(auth) != 2:
|
||||
raise exceptions.AuthenticationFailed('Invalid token header')
|
||||
|
||||
return self.authenticate_credentials(request, auth[1])
|
||||
|
||||
def authenticate_credentials(self, request, access_token):
|
||||
"""
|
||||
:returns: two-tuple of (user, auth) if authentication succeeds, or None otherwise.
|
||||
"""
|
||||
|
||||
# authenticate the client
|
||||
oauth2_client_form = oauth2_provider.forms.ClientAuthForm(request.REQUEST)
|
||||
if not oauth2_client_form.is_valid():
|
||||
raise exceptions.AuthenticationFailed("Client could not be validated")
|
||||
client = oauth2_client_form.cleaned_data.get('client')
|
||||
|
||||
# retrieve the `oauth2_provider.models.OAuth2AccessToken` instance from the access_token
|
||||
auth_backend = oauth2_provider.backends.AccessTokenBackend()
|
||||
token = auth_backend.authenticate(access_token, client)
|
||||
if token is None:
|
||||
raise exceptions.AuthenticationFailed("Invalid token") # does not exist or is expired
|
||||
|
||||
# TODO check scope
|
||||
|
||||
if not self.check_active(token.user):
|
||||
raise exceptions.AuthenticationFailed('User not active: %s' % token.user.username)
|
||||
|
||||
if client and token:
|
||||
request.user = token.user
|
||||
return (request.user, None)
|
||||
|
||||
raise exceptions.AuthenticationFailed(
|
||||
'You are not allowed to access this resource.')
|
||||
|
||||
def authenticate_header(self, request):
|
||||
"""
|
||||
Bearer is the only finalized type currently
|
||||
|
||||
Check details on the `OAuth2Authentication.authenticate` method
|
||||
"""
|
||||
return 'Bearer'
|
||||
|
||||
def check_active(self, user):
|
||||
"""
|
||||
Ensures the user has an active account.
|
||||
|
||||
Optimized for the ``django.contrib.auth.models.User`` case.
|
||||
"""
|
||||
if not self.require_active:
|
||||
# Ignore & move on.
|
||||
return True
|
||||
|
||||
return user.is_active
|
||||
|
|
|
@ -426,3 +426,17 @@ try:
|
|||
import defusedxml.ElementTree as etree
|
||||
except ImportError:
|
||||
etree = None
|
||||
|
||||
|
||||
# OAuth 2 support is optional
|
||||
try:
|
||||
import provider.oauth2 as oauth2_provider
|
||||
|
||||
# Hack to fix submodule import issues
|
||||
submodules = ['backends', 'forms','managers','models','urls','views']
|
||||
for s in submodules:
|
||||
mod = __import__('provider.oauth2.%s.*' % s)
|
||||
setattr(oauth2_provider, s, mod)
|
||||
|
||||
except ImportError:
|
||||
oauth2_provider = None
|
||||
|
|
|
@ -97,9 +97,19 @@ INSTALLED_APPS = (
|
|||
# 'django.contrib.admindocs',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'rest_framework.tests'
|
||||
'rest_framework.tests',
|
||||
)
|
||||
|
||||
try:
|
||||
import provider
|
||||
INSTALLED_APPS += (
|
||||
'provider',
|
||||
'provider.oauth2',
|
||||
)
|
||||
except ImportError:
|
||||
import logging
|
||||
logging.warning("django-oauth2-provider is not install, some tests will be skipped")
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
PASSWORD_HASHERS = (
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse
|
||||
from django.test import Client, TestCase
|
||||
from django.utils import unittest
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import exceptions
|
||||
from rest_framework import permissions
|
||||
|
@ -11,13 +13,16 @@ from rest_framework.authentication import (
|
|||
BaseAuthentication,
|
||||
TokenAuthentication,
|
||||
BasicAuthentication,
|
||||
SessionAuthentication
|
||||
SessionAuthentication,
|
||||
OAuth2Authentication
|
||||
)
|
||||
from rest_framework.compat import patterns
|
||||
from rest_framework.compat import patterns, url, include
|
||||
from rest_framework.compat import oauth2_provider
|
||||
from rest_framework.tests.utils import RequestFactory
|
||||
from rest_framework.views import APIView
|
||||
import json
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
|
||||
factory = RequestFactory()
|
||||
|
@ -43,6 +48,12 @@ urlpatterns = patterns('',
|
|||
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
|
||||
)
|
||||
|
||||
if oauth2_provider is not None:
|
||||
urlpatterns += patterns('',
|
||||
url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')),
|
||||
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
|
||||
)
|
||||
|
||||
|
||||
class BasicAuthTests(TestCase):
|
||||
"""Basic authentication"""
|
||||
|
@ -222,3 +233,129 @@ class IncorrectCredentialsTests(TestCase):
|
|||
response = view(request)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.data, {'detail': 'Bad credentials'})
|
||||
|
||||
|
||||
class OAuth2Tests(TestCase):
|
||||
"""OAuth 2.0 authentication"""
|
||||
urls = 'rest_framework.tests.authentication'
|
||||
|
||||
def setUp(self):
|
||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||
self.username = 'john'
|
||||
self.email = 'lennon@thebeatles.com'
|
||||
self.password = 'password'
|
||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||
|
||||
self.CLIENT_ID = 'client_key'
|
||||
self.CLIENT_SECRET = 'client_secret'
|
||||
self.ACCESS_TOKEN = "access_token"
|
||||
self.REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
self.oauth2_client = oauth2_provider.models.Client.objects.create(
|
||||
client_id=self.CLIENT_ID,
|
||||
client_secret=self.CLIENT_SECRET,
|
||||
redirect_uri='',
|
||||
client_type=0,
|
||||
name='example',
|
||||
user=None,
|
||||
)
|
||||
|
||||
self.access_token = oauth2_provider.models.AccessToken.objects.create(
|
||||
token=self.ACCESS_TOKEN,
|
||||
client=self.oauth2_client,
|
||||
user=self.user,
|
||||
)
|
||||
self.refresh_token = oauth2_provider.models.RefreshToken.objects.create(
|
||||
user=self.user,
|
||||
access_token=self.access_token,
|
||||
client=self.oauth2_client
|
||||
)
|
||||
|
||||
def _create_authorization_header(self, token=None):
|
||||
return "Bearer {0}".format(token or self.access_token.token)
|
||||
|
||||
def _client_credentials_params(self):
|
||||
return {'client_id': self.CLIENT_ID, 'client_secret': self.CLIENT_SECRET}
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_get_form_with_wrong_authorization_header_token_type_failing(self):
|
||||
"""Ensure that a wrong token type lead to the correct HTTP error status code"""
|
||||
auth = "Wrong token-type-obsviously"
|
||||
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
params = self._client_credentials_params()
|
||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_get_form_with_wrong_authorization_header_token_format_failing(self):
|
||||
"""Ensure that a wrong token format lead to the correct HTTP error status code"""
|
||||
auth = "Bearer wrong token format"
|
||||
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
params = self._client_credentials_params()
|
||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_get_form_with_wrong_authorization_header_token_failing(self):
|
||||
"""Ensure that a wrong token lead to the correct HTTP error status code"""
|
||||
auth = "Bearer wrong-token"
|
||||
response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
params = self._client_credentials_params()
|
||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_get_form_with_wrong_client_data_failing_auth(self):
|
||||
"""Ensure GETing form over OAuth with incorrect client credentials fails"""
|
||||
auth = self._create_authorization_header()
|
||||
params = self._client_credentials_params()
|
||||
params['client_id'] += 'a'
|
||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_get_form_passing_auth(self):
|
||||
"""Ensure GETing form over OAuth with correct client credentials succeed"""
|
||||
auth = self._create_authorization_header()
|
||||
params = self._client_credentials_params()
|
||||
response = self.csrf_client.get('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_post_form_passing_auth(self):
|
||||
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
|
||||
auth = self._create_authorization_header()
|
||||
params = self._client_credentials_params()
|
||||
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_post_form_token_removed_failing_auth(self):
|
||||
"""Ensure POSTing when there is no OAuth access token in db fails"""
|
||||
self.access_token.delete()
|
||||
auth = self._create_authorization_header()
|
||||
params = self._client_credentials_params()
|
||||
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_post_form_with_refresh_token_failing_auth(self):
|
||||
"""Ensure POSTing with refresh token instead of access token fails"""
|
||||
auth = self._create_authorization_header(token=self.refresh_token.token)
|
||||
params = self._client_credentials_params()
|
||||
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
||||
|
||||
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
|
||||
def test_post_form_with_expired_access_token_failing_auth(self):
|
||||
"""Ensure POSTing with expired access token fails with an 'Invalid token' error"""
|
||||
self.access_token.expires = datetime.datetime.now() - datetime.timedelta(seconds=10) # 10 seconds late
|
||||
self.access_token.save()
|
||||
auth = self._create_authorization_header()
|
||||
params = self._client_credentials_params()
|
||||
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
|
||||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
|
||||
self.assertIn('Invalid token', response.content)
|
||||
|
|
10
tox.ini
10
tox.ini
|
@ -8,46 +8,52 @@ commands = {envpython} rest_framework/runtests/runtests.py
|
|||
[testenv:py3.3-django1.5]
|
||||
basepython = python3.3
|
||||
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
|
||||
|
||||
[testenv:py3.2-django1.5]
|
||||
basepython = python3.2
|
||||
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
|
||||
|
||||
[testenv:py2.7-django1.5]
|
||||
basepython = python2.7
|
||||
deps = django==1.5
|
||||
django-filter==0.5.4
|
||||
django-oauth2-provider==0.2.3
|
||||
|
||||
[testenv:py2.6-django1.5]
|
||||
basepython = python2.6
|
||||
deps = django==1.5
|
||||
django-filter==0.5.4
|
||||
defusedxml==0.3
|
||||
django-oauth2-provider==0.2.3
|
||||
|
||||
[testenv:py2.7-django1.4]
|
||||
basepython = python2.7
|
||||
deps = django==1.4.3
|
||||
django-filter==0.5.4
|
||||
defusedxml==0.3
|
||||
django-oauth2-provider==0.2.3
|
||||
|
||||
[testenv:py2.6-django1.4]
|
||||
basepython = python2.6
|
||||
deps = django==1.4.3
|
||||
django-filter==0.5.4
|
||||
defusedxml==0.3
|
||||
django-oauth2-provider==0.2.3
|
||||
|
||||
[testenv:py2.7-django1.3]
|
||||
basepython = python2.7
|
||||
deps = django==1.3.5
|
||||
django-filter==0.5.4
|
||||
defusedxml==0.3
|
||||
django-oauth2-provider==0.2.3
|
||||
|
||||
[testenv:py2.6-django1.3]
|
||||
basepython = python2.6
|
||||
deps = django==1.3.5
|
||||
django-filter==0.5.4
|
||||
defusedxml==0.3
|
||||
django-oauth2-provider==0.2.3
|
||||
|
|
Loading…
Reference in New Issue
Block a user