Repository: jazzband/django-oauth-toolkit

Issue:
Django REST Framework - Browseable API 
According to [this question on stackoverflow](http://stackoverflow.com/questions/24387009/django-restframework-browsable-api-login-with-ouath) it should be possible to use the browseable API from the Django REST Framework package.

However, declaring the `SessionAuthentication` authentication class didn't work, I get an error that I'm lacking permission:

```
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': ( 
        'oauth2_provider.ext.rest_framework.OAuth2Authentication', 
        'rest_framework.authentication.SessionAuthentication', 
    ), 
    'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
    'PAGE_SIZE': 10,
    'TEST_REQUEST_DEFAULT_FORMAT': 'json',
}
```

These are my scopes:

```
OAUTH2_PROVIDER = {
    # this is the list of available scopes
    'SCOPES': {
        'read': 'Read scope',
        'write': 'Write scope',
        'foods': 'Access to Foods'
    }
}
```

and this is my view that cannot be accessed through the browseable API if the `TokenHasScope` permission class has been set:

```
class FoodGroupViewSet(ViewSetBase):
    permission_classes = [IsAuthenticated, TokenHasScope]
    required_scopes = ['foods']
    queryset = FoodGroup.objects.all()
    serializer_class = FoodGroupSerializer`
```

Is this a bug or am I doing anything wrong here?



Discussion:
I am experiencing the same issue. 

Django==1.8.5
djangorestframework==3.3.0
django-oauth-toolkit==0.9.0

@marteinn
@0x8DEADF00D

`TokenHasScope` permission fails because there is no token when you authenticate with `SessionAuthentication`. The permission class does not take into account if you are authenticating with the session or with an access_token and I think it's  a sane default.

If you want to avoid the check when authenticating with a non OAuth2 backend you can use this permission class:

``` python
from oauth2_provider.ext.rest_framework import OAuth2Authentication, TokenHasScope

class WeakTokenHasSope(TokenHasScope):
    def has_permission(self, request, view):
        if request.successful_authenticator != OAuth2Authentication:
            return True
        return super().has_permission(request, view)
```

@synasius Shouldn't that be `if not isinstance(request.successful_authenticator, OAuth2Authentication)`?


Resolving patch:
diff --git a/oauth2_provider/ext/rest_framework/__init__.py b/oauth2_provider/ext/rest_framework/__init__.py
--- a/oauth2_provider/ext/rest_framework/__init__.py
+++ b/oauth2_provider/ext/rest_framework/__init__.py
@@ -1,3 +1,4 @@
 # flake8: noqa
 from .authentication import OAuth2Authentication
 from .permissions import TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope
+from .permissions import IsAuthenticatedOrTokenHasScope
diff --git a/oauth2_provider/ext/rest_framework/permissions.py b/oauth2_provider/ext/rest_framework/permissions.py
--- a/oauth2_provider/ext/rest_framework/permissions.py
+++ b/oauth2_provider/ext/rest_framework/permissions.py
@@ -2,7 +2,8 @@
 
 from django.core.exceptions import ImproperlyConfigured
 
-from rest_framework.permissions import BasePermission
+from rest_framework.permissions import BasePermission, IsAuthenticated
+from .authentication import OAuth2Authentication
 
 from ...settings import oauth2_settings
 
@@ -29,7 +30,7 @@ def has_permission(self, request, view):
 
             return token.is_valid(required_scopes)
 
-        assert False, ('TokenHasScope requires either the'
+        assert False, ('TokenHasScope requires the'
                        '`oauth2_provider.rest_framework.OAuth2Authentication` authentication '
                        'class to be used.')
 
@@ -84,3 +85,23 @@ def get_scopes(self, request, view):
         ]
 
         return required_scopes
+
+
+class IsAuthenticatedOrTokenHasScope(BasePermission):
+    """
+    The user is authenticated using some backend or the token has the right scope
+    This only returns True if the user is authenticated, but not using a token
+    or using a token, and the token has the correct scope.
+
+    This is usefull when combined with the DjangoModelPermissions to allow people browse the browsable api's
+    if they log in using the a non token bassed middleware,
+    and let them access the api's using a rest client with a token
+    """
+    def has_permission(self, request, view):
+        is_authenticated = IsAuthenticated().has_permission(request, view)
+        oauth2authenticated = False
+        if is_authenticated:
+            oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication)
+
+        token_has_scope = TokenHasScope()
+        return (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view)


Test patch:
diff --git a/oauth2_provider/tests/test_rest_framework.py b/oauth2_provider/tests/test_rest_framework.py
--- a/oauth2_provider/tests/test_rest_framework.py
+++ b/oauth2_provider/tests/test_rest_framework.py
@@ -19,7 +19,9 @@
 try:
     from rest_framework import permissions
     from rest_framework.views import APIView
+    from rest_framework.test import force_authenticate, APIRequestFactory
     from ..ext.rest_framework import OAuth2Authentication, TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope
+    from ..ext.rest_framework import IsAuthenticatedOrTokenHasScope
 
     class MockView(APIView):
         permission_classes = (permissions.IsAuthenticated,)
@@ -37,6 +39,10 @@ class ScopedView(OAuth2View):
         permission_classes = [permissions.IsAuthenticated, TokenHasScope]
         required_scopes = ['scope1']
 
+    class AuthenticatedOrScopedView(OAuth2View):
+        permission_classes = [IsAuthenticatedOrTokenHasScope]
+        required_scopes = ['scope1']
+
     class ReadWriteScopedView(OAuth2View):
         permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope]
 
@@ -51,6 +57,7 @@ class ResourceScopedView(OAuth2View):
         url(r'^oauth2-scoped-test/$', ScopedView.as_view()),
         url(r'^oauth2-read-write-test/$', ReadWriteScopedView.as_view()),
         url(r'^oauth2-resource-scoped-test/$', ResourceScopedView.as_view()),
+        url(r'^oauth2-authenticated-or-scoped-test/$', AuthenticatedOrScopedView.as_view()),
     )
 
     rest_framework_installed = True
@@ -105,6 +112,24 @@ def test_authentication_denied(self):
         response = self.client.get("/oauth2-test/", HTTP_AUTHORIZATION=auth)
         self.assertEqual(response.status_code, 401)
 
+    @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed')
+    def test_authentication_or_scope_denied(self):
+        # user is not authenticated
+        # not a correct token
+        auth = self._create_authorization_header("fake-token")
+        response = self.client.get("/oauth2-authenticated-or-scoped-test/", HTTP_AUTHORIZATION=auth)
+        self.assertEqual(response.status_code, 401)
+        # token doesn't have correct scope
+        auth = self._create_authorization_header(self.access_token.token)
+
+        factory = APIRequestFactory()
+        request = factory.get("/oauth2-authenticated-or-scoped-test/")
+        request.auth = auth
+        force_authenticate(request, token=self.access_token)
+        response = AuthenticatedOrScopedView.as_view()(request)
+        # authenticated but wrong scope, this is 403, not 401
+        self.assertEqual(response.status_code, 403)
+
     @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed')
     def test_scoped_permission_allow(self):
         self.access_token.scope = 'scope1'
@@ -114,6 +139,33 @@ def test_scoped_permission_allow(self):
         response = self.client.get("/oauth2-scoped-test/", HTTP_AUTHORIZATION=auth)
         self.assertEqual(response.status_code, 200)
 
+    @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed')
+    def test_authenticated_or_scoped_permission_allow(self):
+        self.access_token.scope = 'scope1'
+        self.access_token.save()
+        # correct token and correct scope
+        auth = self._create_authorization_header(self.access_token.token)
+        response = self.client.get("/oauth2-authenticated-or-scoped-test/", HTTP_AUTHORIZATION=auth)
+        self.assertEqual(response.status_code, 200)
+
+        auth = self._create_authorization_header("fake-token")
+        # incorrect token  but authenticated
+        factory = APIRequestFactory()
+        request = factory.get("/oauth2-authenticated-or-scoped-test/")
+        request.auth = auth
+        force_authenticate(request, self.test_user)
+        response = AuthenticatedOrScopedView.as_view()(request)
+        self.assertEqual(response.status_code, 200)
+
+        # correct token  but not authenticated
+        request = factory.get("/oauth2-authenticated-or-scoped-test/")
+        request.auth = auth
+        self.access_token.scope = 'scope1'
+        self.access_token.save()
+        force_authenticate(request, token=self.access_token)
+        response = AuthenticatedOrScopedView.as_view()(request)
+        self.assertEqual(response.status_code, 200)
+
     @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed')
     def test_scoped_permission_deny(self):
         self.access_token.scope = 'scope2'