Determine WWW-Authenticate header using all authentication classes

When trying to determine the WWW-Authenticate header, previously the
first available authentication class was used. If it didn't provide any
header (see the `SessionAuthentication` class as an example), `None` was
returned, even though there might be another authentication class which
does provide a valid header.

With this commit, the code that tries to determine the
WWW-Authentication header iterates through all available authentication
classes, until it finds one that provides a valid header (if any).
This commit is contained in:
Danilo Bargen 2014-06-01 23:04:35 +02:00
parent 12394c9cac
commit cf2cf5c3b5
4 changed files with 47 additions and 7 deletions

View File

@ -83,7 +83,7 @@ When an unauthenticated request is denied permission there are two different err
HTTP 401 responses must always include a `WWW-Authenticate` header, that instructs the client how to authenticate. HTTP 403 responses do not include the `WWW-Authenticate` header.
The kind of response that will be used depends on the authentication scheme. Although multiple authentication schemes may be in use, only one scheme may be used to determine the type of response. **The first authentication class set on the view is used when determining the type of response**.
The kind of response that will be used depends on the authentication scheme. Although multiple authentication schemes may be in use, only one scheme may be used to determine the type of response. The first authentication class set on the view that can provide a valid `WWW-Authenticate` header is used when determining the type of response.
Note that when a request may successfully authenticate, but still be denied permission to perform the request, in which case a `403 Permission Denied` response will always be used, regardless of the authentication scheme.

View File

@ -51,7 +51,9 @@ urlpatterns = patterns('',
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
(r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
(r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication],
permission_classes=[permissions.TokenHasReadWriteScope]))
permission_classes=[permissions.TokenHasReadWriteScope])),
(r'^session-and-basic/$', MockView.as_view(
authentication_classes=[SessionAuthentication, BasicAuthentication])),
)
class OAuth2AuthenticationDebug(OAuth2Authentication):
@ -632,6 +634,37 @@ class OAuth2Tests(TestCase):
self.assertEqual(response.status_code, 200)
class SessionAndBasicAuthTests(TestCase):
"""Session + basic authentication"""
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
self.csrf_client = APIClient(enforce_csrf_checks=True)
self.non_csrf_client = APIClient(enforce_csrf_checks=False)
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'
self.user = User.objects.create_user(self.username, self.email, self.password)
def test_post_form_basic_auth_passing(self):
credentials = ('%s:%s' % (self.username, self.password))
base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
auth = 'Basic %s' % base64_credentials
response = self.csrf_client.post('/session-and-basic/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_post_form_session_auth_passing(self):
self.non_csrf_client.login(username=self.username, password=self.password)
response = self.non_csrf_client.post('/session-and-basic/', {'example': 'example'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_post_form_no_auth_failing(self):
response = self.non_csrf_client.post('/session-and-basic/', {'example': 'example'})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
response_headers = dict(response.items())
self.assertEqual(response_headers.get('WWW-Authenticate'), 'Basic realm="api"')
class FailingAuthAccessedInRenderer(TestCase):
def setUp(self):
class AuthAccessingRenderer(renderers.BaseRenderer):

View File

@ -1,9 +1,9 @@
from __future__ import unicode_literals
from django.test import TestCase
from rest_framework import status
from rest_framework.authentication import BasicAuthentication
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
from rest_framework.parsers import JSONParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework.test import APIRequestFactory
@ -132,8 +132,13 @@ class DecoratorTestCase(TestCase):
def test_permission_classes(self):
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@authentication_classes([SessionAuthentication])
@permission_classes([IsAdminUser])
def view(request):
self.assertEqual(len(request.permission_classes), 1)
self.assertTrue(isinstance(request.permission_classes[0],
IsAdminUser))
return Response({})
request = self.factory.get('/')

View File

@ -147,8 +147,10 @@ class APIView(View):
header to use for 401 responses, if any.
"""
authenticators = self.get_authenticators()
if authenticators:
return authenticators[0].authenticate_header(request)
for authenticator in authenticators:
header = authenticator.authenticate_header(request)
if header:
return header
def get_parser_context(self, http_request):
"""