diff options
| author | Tom Christie | 2013-02-28 17:58:58 +0000 | 
|---|---|---|
| committer | Tom Christie | 2013-02-28 17:58:58 +0000 | 
| commit | 13b3af0d22bdbae5be0eb39ea50219c1fb83e28f (patch) | |
| tree | 26faec0481b8bb82e726ac4fc109b5f65d292a82 | |
| parent | 4e14b26fa9727a79f8ae7c7ef25d1339500fa26c (diff) | |
| download | django-rest-framework-13b3af0d22bdbae5be0eb39ea50219c1fb83e28f.tar.bz2 | |
Auth is no longer lazy.  Closes #667.
More consistent auth failure behavior.
| -rw-r--r-- | docs/api-guide/authentication.md | 4 | ||||
| -rw-r--r-- | rest_framework/tests/authentication.py | 37 | ||||
| -rw-r--r-- | rest_framework/views.py | 11 | 
3 files changed, 49 insertions, 3 deletions
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 342fabe7..fae86386 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -10,7 +10,7 @@ Authentication is the mechanism of associating an incoming request with a set of  REST framework provides a number of authentication schemes out of the box, and also allows you to implement custom schemes. -Authentication will run the first time either the `request.user` or `request.auth` properties are accessed, and determines how those properties are initialized. +Authentication is always run at the very start of the view, before the permission and throttling checks occur, and before any other code is allowed to proceed.  The `request.user` property will typically be set to an instance of the `contrib.auth` package's `User` class. @@ -191,7 +191,7 @@ In some circumstances instead of returning `None`, you may want to raise an `Aut  Typically the approach you should take is:  * If authentication is not attempted, return `None`.  Any other authentication schemes also in use will still be checked. -* If authentication is attempted but fails, raise a `AuthenticationFailed` exception.  An error response will be returned immediately, without checking any other authentication schemes. +* If authentication is attempted but fails, raise a `AuthenticationFailed` exception.  An error response will be returned immediately, regardless of any permissions checks, and without checking any other authentication schemes.  You *may* also override the `.authenticate_header(self, request)` method.  If implemented, it should return a string that will be used as the value of the `WWW-Authenticate` header in a `HTTP 401 Unauthorized` response. diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 2a2bfba9..7b754af5 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -3,25 +3,39 @@ 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 +from rest_framework import exceptions  from rest_framework import permissions  from rest_framework import status  from rest_framework.authtoken.models import Token -from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication +from rest_framework.authentication import ( +    BaseAuthentication, +    TokenAuthentication, +    BasicAuthentication, +    SessionAuthentication +)  from rest_framework.compat import patterns +from rest_framework.tests.utils import RequestFactory  from rest_framework.views import APIView  import json  import base64 +factory = RequestFactory() + +  class MockView(APIView):      permission_classes = (permissions.IsAuthenticated,) +    def get(self, request): +        return HttpResponse({'a': 1, 'b': 2, 'c': 3}) +      def post(self, request):          return HttpResponse({'a': 1, 'b': 2, 'c': 3})      def put(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])), @@ -187,3 +201,24 @@ class TokenAuthTests(TestCase):                                 {'username': self.username, 'password': self.password})          self.assertEqual(response.status_code, status.HTTP_200_OK)          self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key) + + +class IncorrectCredentialsTests(TestCase): +    def test_incorrect_credentials(self): +        """ +        If a request contains bad authentication credentials, then +        authentication should run and error, even if no permissions +        are set on the view. +        """ +        class IncorrectCredentialsAuth(BaseAuthentication): +            def authenticate(self, request): +                raise exceptions.AuthenticationFailed('Bad credentials') + +        request = factory.get('/') +        view = MockView.as_view( +            authentication_classes=(IncorrectCredentialsAuth,), +            permission_classes=() +        ) +        response = view(request) +        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) +        self.assertEqual(response.data, {'detail': 'Bad credentials'}) diff --git a/rest_framework/views.py b/rest_framework/views.py index fa742582..81cbdcbb 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -257,6 +257,16 @@ class APIView(View):                  return (renderers[0], renderers[0].media_type)              raise +    def perform_authentication(self, request): +        """ +        Perform authentication on the incoming request. + +        Note that if you override this and simply 'pass', then authentication +        will instead be performed lazily, the first time either +        `request.user` or `request.auth` is accessed. +        """ +        request.user +      def check_permissions(self, request):          """          Check if the request should be permitted. @@ -305,6 +315,7 @@ class APIView(View):          self.format_kwarg = self.get_format_suffix(**kwargs)          # Ensure that the incoming request is permitted +        self.perform_authentication(request)          self.check_permissions(request)          self.check_throttles(request)  | 
