-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OAuth 1.0a authentication #678
Changes from 9 commits
1062d71
ced22db
1aed9c1
e2b11a2
cfce455
5d9ed34
59a6f5f
d84c2cf
a430445
dd355d5
55ea5b9
2eabc5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -182,6 +182,20 @@ 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. | ||
|
||
## OAuthAuthentication | ||
|
||
This authentication uses [OAuth 1.0](http://tools.ietf.org/html/rfc5849) authentication scheme. It depends on optional `django-oauth-plus` and `oauth2` packages. In order to make it work you must istall these packages and add `oauth_provider` (from `django-oauth-plus`) to your `INSTALLED_APPS`: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note link style in docs is to not use inline linking. Use square bracket for both text and marker and a descriptive anchor eg 'rfc5849' |
||
|
||
INSTALLED_APPS = ( | ||
#(...) | ||
`oauth_provider`, | ||
) | ||
|
||
OAuthAuthentication class provides only 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 Reqest/Access Tokens. This is because there are many different OAuth flows in use. Almost always they require end-user interaction, and most likely this is what you want to design yourself. | ||
|
||
Luckily `django-oauth-plus` provides simple foundation for classic 'three-legged' oauth flow, so if it is what you need please refer to [its documentation](http://code.larlet.fr/django-oauth-plus/wiki/Home). This documentation will provide you also information about how to work with supplied models and change basic settings. | ||
|
||
|
||
# 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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,5 @@ markdown>=2.1.0 | |
PyYAML>=3.10 | ||
defusedxml>=0.3 | ||
django-filter>=0.5.4 | ||
django-oauth-plus>=2.0 | ||
oauth2>=1.5.211 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,15 +2,18 @@ | |
from django.contrib.auth.models import User | ||
from django.http import HttpResponse | ||
from django.test import Client, TestCase | ||
from rest_framework import HTTP_HEADER_ENCODING | ||
import time | ||
from rest_framework import HTTP_HEADER_ENCODING, status | ||
from rest_framework import permissions | ||
from rest_framework.authtoken.models import Token | ||
from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication | ||
from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication, OAuthAuthentication | ||
from rest_framework.compat import patterns | ||
from rest_framework.views import APIView | ||
from rest_framework.compat import oauth | ||
from rest_framework.compat import oauth_provider | ||
import json | ||
import base64 | ||
|
||
import unittest | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to import the compat version, so this works across different py versions. See existing cases of skipUnless in tests. |
||
|
||
class MockView(APIView): | ||
permission_classes = (permissions.IsAuthenticated,) | ||
|
@@ -21,11 +24,15 @@ def post(self, request): | |
def put(self, request): | ||
return HttpResponse({'a': 1, 'b': 2, 'c': 3}) | ||
|
||
def get(self, request): | ||
return HttpResponse({'a': 1, 'b': 2, 'c': 3}) | ||
|
||
urlpatterns = patterns('', | ||
(r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])), | ||
(r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])), | ||
(r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])), | ||
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'), | ||
(r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])) | ||
) | ||
|
||
|
||
|
@@ -186,3 +193,158 @@ def test_token_login_form(self): | |
{'username': self.username, 'password': self.password}) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key) | ||
|
||
class OAuthTests(TestCase): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests should be excluded unless both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now are skipped. |
||
"""OAuth 1.0a authentication""" | ||
urls = 'rest_framework.tests.authentication' | ||
|
||
def setUp(self): | ||
# these imports are here because oauth is optional and hiding them in try..except block or compat | ||
# could obscure problems if something breaks | ||
from oauth_provider.models import Consumer, Resource | ||
from oauth_provider.models import Token as OAuthToken | ||
from oauth_provider import consts | ||
|
||
self.consts = consts | ||
|
||
self.csrf_client = Client(enforce_csrf_checks=True) | ||
self.username = 'john' | ||
self.email = '[email protected]' | ||
self.password = 'password' | ||
self.user = User.objects.create_user(self.username, self.email, self.password) | ||
|
||
self.CONSUMER_KEY = 'consumer_key' | ||
self.CONSUMER_SECRET = 'consumer_secret' | ||
self.TOKEN_KEY = "token_key" | ||
self.TOKEN_SECRET = "token_secret" | ||
|
||
self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, | ||
name='example', user=self.user, status=self.consts.ACCEPTED) | ||
|
||
|
||
self.resource = Resource.objects.create(name="resource name", url="api/") | ||
self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource, | ||
token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True | ||
) | ||
|
||
|
||
def _create_authorization_header(self): | ||
params = { | ||
'oauth_version': "1.0", | ||
'oauth_nonce': oauth.generate_nonce(), | ||
'oauth_timestamp': int(time.time()), | ||
'oauth_token': self.token.key, | ||
'oauth_consumer_key': self.consumer.key | ||
} | ||
|
||
req = oauth.Request(method="GET", url="http://example.com", parameters=params) | ||
|
||
signature_method = oauth.SignatureMethod_PLAINTEXT() | ||
req.sign_request(signature_method, self.consumer, self.token) | ||
|
||
return req.to_header()["Authorization"] | ||
|
||
def _create_authorization_url_parameters(self): | ||
params = { | ||
'oauth_version': "1.0", | ||
'oauth_nonce': oauth.generate_nonce(), | ||
'oauth_timestamp': int(time.time()), | ||
'oauth_token': self.token.key, | ||
'oauth_consumer_key': self.consumer.key | ||
} | ||
|
||
req = oauth.Request(method="GET", url="http://example.com", parameters=params) | ||
|
||
signature_method = oauth.SignatureMethod_PLAINTEXT() | ||
req.sign_request(signature_method, self.consumer, self.token) | ||
return dict(req) | ||
|
||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') | ||
@unittest.skipUnless(oauth, 'oauth2 not installed') | ||
def test_post_form_passing_oauth(self): | ||
"""Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF""" | ||
auth = self._create_authorization_header() | ||
response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') | ||
@unittest.skipUnless(oauth, 'oauth2 not installed') | ||
def test_post_form_repeated_nonce_failing_oauth(self): | ||
"""Ensure POSTing form over OAuth with repeated auth (same nonces and timestamp) credentials fails""" | ||
auth = self._create_authorization_header() | ||
response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
# simulate reply attack auth header containes already used (nonce, timestamp) pair | ||
response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) | ||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) | ||
|
||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') | ||
@unittest.skipUnless(oauth, 'oauth2 not installed') | ||
def test_post_form_token_removed_failing_oauth(self): | ||
"""Ensure POSTing when there is no OAuth access token in db fails""" | ||
self.token.delete() | ||
auth = self._create_authorization_header() | ||
response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) | ||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) | ||
|
||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') | ||
@unittest.skipUnless(oauth, 'oauth2 not installed') | ||
def test_post_form_consumer_status_not_accepted_failing_oauth(self): | ||
"""Ensure POSTing when consumer status is anything other than ACCEPTED fails""" | ||
for consumer_status in (self.consts.CANCELED, self.consts.PENDING, self.consts.REJECTED): | ||
self.consumer.status = consumer_status | ||
self.consumer.save() | ||
|
||
auth = self._create_authorization_header() | ||
response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) | ||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) | ||
|
||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') | ||
@unittest.skipUnless(oauth, 'oauth2 not installed') | ||
def test_post_form_with_request_token_failing_oauth(self): | ||
"""Ensure POSTing with unauthorized request token instead of access token fails""" | ||
self.token.token_type = self.token.REQUEST | ||
self.token.save() | ||
|
||
auth = self._create_authorization_header() | ||
response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth) | ||
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) | ||
|
||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') | ||
@unittest.skipUnless(oauth, 'oauth2 not installed') | ||
def test_post_form_with_urlencoded_parameters(self): | ||
"""Ensure POSTing with x-www-form-urlencoded auth parameters passes""" | ||
params = self._create_authorization_url_parameters() | ||
response = self.csrf_client.post('/oauth/', params) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') | ||
@unittest.skipUnless(oauth, 'oauth2 not installed') | ||
def test_get_form_with_url_parameters(self): | ||
"""Ensure GETing with auth in url parameters passes""" | ||
params = self._create_authorization_url_parameters() | ||
response = self.csrf_client.get('/oauth/', params) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') | ||
@unittest.skipUnless(oauth, 'oauth2 not installed') | ||
def test_post_hmac_sha1_signature_passes(self): | ||
"""Ensure POSTing using HMAC_SHA1 signature method passes""" | ||
params = { | ||
'oauth_version': "1.0", | ||
'oauth_nonce': oauth.generate_nonce(), | ||
'oauth_timestamp': int(time.time()), | ||
'oauth_token': self.token.key, | ||
'oauth_consumer_key': self.consumer.key | ||
} | ||
|
||
req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params) | ||
|
||
signature_method = oauth.SignatureMethod_HMAC_SHA1() | ||
req.sign_request(signature_method, self.consumer, self.token) | ||
auth = req.to_header()["Authorization"] | ||
|
||
response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth) | ||
self.assertEqual(response.status_code, 200) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs to only install these on Python 2.x