Merge pull request #249 from mjumbewu/restframework2

Implement simple token authentication
This commit is contained in:
Tom Christie 2012-09-07 13:48:39 -07:00
commit 9faca0aef0
8 changed files with 115 additions and 26 deletions

View File

@ -103,4 +103,38 @@ class SessionAuthentication(BaseAuthentication):
return (user, None) return (user, None)
# TODO: TokenAuthentication, DigestAuthentication, OAuthAuthentication class TokenAuthentication(BaseAuthentication):
"""
Use a token model for authentication.
A custom token model may be used here, but must have the following minimum
properties:
* key -- The string identifying the token
* user -- The user to which the token belongs
* revoked -- The status of the token
The token key should be passed in as a string to the "Authorization" HTTP
header. For example:
Authorization: 0123456789abcdef0123456789abcdef
"""
model = None
def authenticate(self, request):
key = request.META.get('HTTP_AUTHORIZATION', '').strip()
if self.model is None:
from djangorestframework.tokenauth.models import BasicToken
self.model = BasicToken
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
return None
if token.user.is_active and not token.revoked:
return (token.user, token)
# TODO: DigestAuthentication, OAuthAuthentication

View File

@ -90,6 +90,7 @@ INSTALLED_APPS = (
# Uncomment the next line to enable admin documentation: # Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs', # 'django.contrib.admindocs',
'djangorestframework', 'djangorestframework',
'djangorestframework.tokenauth',
) )
STATIC_URL = '/static/' STATIC_URL = '/static/'

View File

@ -8,6 +8,9 @@ from django.http import HttpResponse
from djangorestframework.views import APIView from djangorestframework.views import APIView
from djangorestframework import permissions from djangorestframework import permissions
from djangorestframework.tokenauth.models import BasicToken
from djangorestframework.authentication import TokenAuthentication
import base64 import base64
@ -20,6 +23,8 @@ class MockView(APIView):
def put(self, request): def put(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3}) return HttpResponse({'a': 1, 'b': 2, 'c': 3})
MockView.authentication += (TokenAuthentication,)
urlpatterns = patterns('', urlpatterns = patterns('',
(r'^$', MockView.as_view()), (r'^$', MockView.as_view()),
) )
@ -104,3 +109,45 @@ class SessionAuthTests(TestCase):
""" """
response = self.csrf_client.post('/', {'example': 'example'}) response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
class TokenAuthTests(TestCase):
"""Token authentication"""
urls = 'djangorestframework.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.key = 'abcd1234'
self.token = BasicToken.objects.create(key=self.key, user=self.user)
def test_post_form_passing_token_auth(self):
"""Ensure POSTing json over token auth with correct credentials passes and does not require CSRF"""
auth = self.key
response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
def test_post_json_passing_token_auth(self):
"""Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
auth = self.key
response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
def test_post_form_failing_token_auth(self):
"""Ensure POSTing form over token auth without correct credentials fails"""
response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403)
def test_post_json_failing_token_auth(self):
"""Ensure POSTing json over token auth without correct credentials fails"""
response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json')
self.assertEqual(response.status_code, 403)
def test_token_has_auto_assigned_key_if_none_provided(self):
"""Ensure creating a token with no key will auto-assign a key"""
token = BasicToken.objects.create(user=self.user)
self.assertEqual(len(token.key), 32)

View File

@ -55,27 +55,27 @@ class MockView(APIView):
def get(self, request, **kwargs): def get(self, request, **kwargs):
response = Response(DUMMYCONTENT, status=DUMMYSTATUS) response = Response(DUMMYCONTENT, status=DUMMYSTATUS)
return self.render(response) return response
class MockGETView(APIView): class MockGETView(APIView):
def get(self, request, **kwargs): def get(self, request, **kwargs):
return {'foo': ['bar', 'baz']} return Response({'foo': ['bar', 'baz']})
class HTMLView(APIView): class HTMLView(APIView):
renderers = (DocumentingHTMLRenderer, ) renderers = (DocumentingHTMLRenderer, )
def get(self, request, **kwargs): def get(self, request, **kwargs):
return 'text' return Response('text')
class HTMLView1(APIView): class HTMLView1(APIView):
renderers = (DocumentingHTMLRenderer, JSONRenderer) renderers = (DocumentingHTMLRenderer, JSONRenderer)
def get(self, request, **kwargs): def get(self, request, **kwargs):
return 'text' return Response('text')
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])), url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
@ -88,7 +88,7 @@ urlpatterns = patterns('',
) )
class RendererIntegrationTests(TestCase): class RendererEndToEndTests(TestCase):
""" """
End-to-end testing of renderers using an RendererMixin on a generic view. End-to-end testing of renderers using an RendererMixin on a generic view.
""" """
@ -216,18 +216,6 @@ class JSONRendererTests(TestCase):
self.assertEquals(strip_trailing_whitespace(content), _indented_repr) self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
class MockGETView(APIView):
def get(self, request, *args, **kwargs):
return Response({'foo': ['bar', 'baz']})
urlpatterns = patterns('',
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderers=[JSONRenderer, JSONPRenderer])),
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])),
)
class JSONPRendererTests(TestCase): class JSONPRendererTests(TestCase):
""" """
Tests specific to the JSONP Renderer Tests specific to the JSONP Renderer

View File

@ -0,0 +1,15 @@
import uuid
from django.db import models
class BasicToken(models.Model):
"""
The default authorization token model class.
"""
key = models.CharField(max_length=32, primary_key=True, blank=True)
user = models.ForeignKey('auth.User')
revoked = models.BooleanField(default=False)
def save(self, *args, **kwargs):
if not self.key:
self.key = uuid.uuid4().hex
return super(BasicToken, self).save(*args, **kwargs)

View File

View File

@ -8,7 +8,7 @@ Authentication will run the first time either the `request.user` or `request.aut
The `request.user` property will typically be set to an instance of the `contrib.auth` package's `User` class. The `request.user` property will typically be set to an instance of the `contrib.auth` package's `User` class.
The `request.auth` property is used for any additional authentication information, for example, it may be used to represent an authentication token that the request was signed with. The `request.auth` property is used for any additional authentication information, for example, it may be used to represent an authentication token that the request was signed with.
## How authentication is determined ## How authentication is determined
@ -36,7 +36,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi
def get(self, request, format=None): def get(self, request, format=None):
content = { content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance. 'user': unicode(request.user), # `django.contrib.auth.User` instance.
'auth': unicode(request.auth), # None 'auth': unicode(request.auth), # None
} }
return Response(content) return Response(content)
@ -49,7 +49,7 @@ Or, if you're using the `@api_view` decorator with function based views.
) )
def example_view(request, format=None): def example_view(request, format=None):
content = { content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance. 'user': unicode(request.user), # `django.contrib.auth.User` instance.
'auth': unicode(request.auth), # None 'auth': unicode(request.auth), # None
} }
return Response(content) return Response(content)
@ -65,16 +65,20 @@ If successfully authenticated, `UserBasicAuthentication` provides the following
* `request.user` will be a `django.contrib.auth.models.User` instance. * `request.user` will be a `django.contrib.auth.models.User` instance.
* `request.auth` will be `None`. * `request.auth` will be `None`.
## TokenBasicAuthentication ## TokenAuthentication
This policy uses [HTTP Basic Authentication][basicauth], signed against a token key and secret. Token basic authentication is appropriate for client-server setups, such as native desktop and mobile clients. This policy uses [HTTP Authentication][basicauth] with no authentication scheme. Token basic authentication is appropriate for client-server setups, such as native desktop and mobile clients. The token key should be passed in as a string to the "Authorization" HTTP header. For example:
**Note:** If you run `TokenBasicAuthentication` in production your API must be `https` only, or it will be completely insecure. curl http://my.api.org/ -X POST -H "Authorization: 0123456789abcdef0123456789abcdef"
If successfully authenticated, `TokenBasicAuthentication` provides the following credentials. **Note:** If you run `TokenAuthentication` in production your API must be `https` only, or it will be completely insecure.
If successfully authenticated, `TokenAuthentication` provides the following credentials.
* `request.user` will be a `django.contrib.auth.models.User` instance. * `request.user` will be a `django.contrib.auth.models.User` instance.
* `request.auth` will be a `djangorestframework.models.BasicToken` instance. * `request.auth` will be a `djangorestframework.tokenauth.models.BasicToken` instance.
To use the `TokenAuthentication` policy, you must have a token model. Django REST Framework comes with a minimal default token model. To use it, include `djangorestframework.tokenauth` in your installed applications and sync your database. To use your own token model, subclass the `djangorestframework.tokenauth.TokenAuthentication` class and specify a `model` attribute that references your custom token model. The token model must provide `user`, `key`, and `revoked` attributes. Refer to the `djangorestframework.tokenauth.models.BasicToken` model as an example.
## OAuthAuthentication ## OAuthAuthentication