Repository: encode/django-rest-framework

Issue:
[oauth2] client_id and client_secret shouldn't be needed in all requests
The docs[0] says:

> The only thing needed to make the OAuth2Authentication class work is to insert the access_token you've received in the Authorization request header.

But the authenticate_credentials[1] always check for client_id and client_secret, and use user from it to check the token. 
If client_id and client_secret are not informed, the form is invalid because client_id and client_secret are required.

The client_id and client_secret should be only required for requiring the token, right?

[0] http://django-rest-framework.org/api-guide/authentication.html
[1] https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/authentication.py#L320



Discussion:
@dulaccc one for you I think? :p

Btw. I haven't actually used OAuth2 myself yet so I'm going to be a terrible person for helping resolve this ticket.

May be worth reading through [this thread](https://groups.google.com/forum/?fromgroups=#!topic/django-rest-framework/5pB7WIFlDXE) related to implementing OAuth2 to see if that helps at all, and consider raising this question on the mailing list.

The only benefit to check for all requests is that it lets us be sure that the access_token submitted is used with the right client.

But I think you are right @fernandogrd, the client credentials are only needed in case of an access_token request, or a refresh_token request. When [accessing protected resources](https://tools.ietf.org/html/rfc6749#section-7.1) there is indeed no paragraph talking about client credentials.

Also, should token be unique? I find weird that oauth2-provider only have a token + client authentication backend.

Yes that makes sense. And like you said that weird, the [AccessToken backend](https://github.com/caffeinehit/django-oauth2-provider/blob/master/provider/oauth2/backends.py#L64-L74) check for a database match with the `token` and `client` parameter. Like the two combine would be the primary key...

Wrt, we probably need to write our own backend.

It seems it uses shortuuid for generating token, so it is probably unique enough, but maybe enforce it in database is a good idea.

I'm working on a patch


Resolving patch:
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -2,14 +2,16 @@
 Provides a set of pluggable authentication policies.
 """
 from __future__ import unicode_literals
+import base64
+from datetime import datetime
+
 from django.contrib.auth import authenticate
 from django.core.exceptions import ImproperlyConfigured
 from rest_framework import exceptions, HTTP_HEADER_ENCODING
 from rest_framework.compat import CsrfViewMiddleware
 from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
-from rest_framework.compat import oauth2_provider, oauth2_provider_forms, oauth2_provider_backends
+from rest_framework.compat import oauth2_provider, oauth2_provider_forms
 from rest_framework.authtoken.models import Token
-import base64
 
 
 def get_authorization_header(request):
@@ -315,21 +317,15 @@ def authenticate_credentials(self, request, access_token):
         Authenticate the request, given the access token.
         """
 
-        # 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 `OAuth2AccessToken` instance from the access_token
-        auth_backend = oauth2_provider_backends.AccessTokenBackend()
-        token = auth_backend.authenticate(access_token, client)
-        if token is None:
+        try:
+            token = oauth2_provider.models.AccessToken.objects.select_related('user')
+            # TODO: Change to timezone aware datetime when oauth2_provider add
+            # support to it.
+            token = token.get(token=access_token, expires__gt=datetime.now())
+        except oauth2_provider.models.AccessToken.DoesNotExist:
             raise exceptions.AuthenticationFailed('Invalid token')
 
-        user = token.user
-
-        if not user.is_active:
+        if not token.user.is_active:
             msg = 'User inactive or deleted: %s' % user.username
             raise exceptions.AuthenticationFailed(msg)
 
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -445,14 +445,12 @@ def apply_markdown(text):
 # OAuth 2 support is optional
 try:
     import provider.oauth2 as oauth2_provider
-    from provider.oauth2 import backends as oauth2_provider_backends
     from provider.oauth2 import models as oauth2_provider_models
     from provider.oauth2 import forms as oauth2_provider_forms
     from provider import scope as oauth2_provider_scope
     from provider import constants as oauth2_constants
 except ImportError:
     oauth2_provider = None
-    oauth2_provider_backends = None
     oauth2_provider_models = None
     oauth2_provider_forms = None
     oauth2_provider_scope = None


Test patch:
diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py
--- a/rest_framework/tests/authentication.py
+++ b/rest_framework/tests/authentication.py
@@ -466,17 +466,13 @@ def setUp(self):
     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)
+        response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
         self.assertEqual(response.status_code, 401)
 
     @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
@@ -485,8 +481,7 @@ def test_get_form_with_wrong_authorization_header_token_format_failing(self):
         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)
+        response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
         self.assertEqual(response.status_code, 401)
 
     @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
@@ -495,33 +490,21 @@ def test_get_form_with_wrong_authorization_header_token_failing(self):
         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)
+        response = self.csrf_client.get('/oauth2-test/', 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)
+        response = self.csrf_client.get('/oauth2-test/', 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)
+        response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
         self.assertEqual(response.status_code, 200)
 
     @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
@@ -529,16 +512,14 @@ 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)
+        response = self.csrf_client.post('/oauth2-test/', 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)
+        response = self.csrf_client.post('/oauth2-test/', 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')
@@ -547,8 +528,7 @@ def test_post_form_with_expired_access_token_failing_auth(self):
         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)
+        response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
         self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
         self.assertIn('Invalid token', response.content)
 
@@ -559,10 +539,9 @@ def test_post_form_with_invalid_scope_failing_auth(self):
         read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read']
         read_only_access_token.save()
         auth = self._create_authorization_header(token=read_only_access_token.token)
-        params = self._client_credentials_params()
-        response = self.csrf_client.get('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
+        response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
         self.assertEqual(response.status_code, 200)
-        response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
+        response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
     @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
@@ -572,6 +551,5 @@ def test_post_form_with_valid_scope_passing_auth(self):
         read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write']
         read_write_access_token.save()
         auth = self._create_authorization_header(token=read_write_access_token.token)
-        params = self._client_credentials_params()
-        response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
+        response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
         self.assertEqual(response.status_code, 200)