Skip to content

Commit 107e5ea

Browse files
simonschmidtjleclanche
authored andcommitted
Includes required scopes in 403 response
Requires django rest framework >3.5.0 Makes it easier for a client to know which extra scopes to request Resolves #504
1 parent 859c39d commit 107e5ea

File tree

4 files changed

+52
-1
lines changed

4 files changed

+52
-1
lines changed

docs/settings.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ WRITE_SCOPE
152152

153153
The name of the *write* scope.
154154

155+
ERROR_RESPONSE_WITH_SCOPES
156+
~~~~~~~~~~~~~~~~~~~~~~~~~~
157+
When authorization fails due to insufficient scopes include the required scopes in the response.
158+
Only applicable when used with `Django REST Framework <http://django-rest-framework.org/>`_
159+
155160
RESOURCE_SERVER_INTROSPECTION_URL
156161
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
157162
The introspection endpoint for validating token remotely (RFC7662).

oauth2_provider/contrib/rest_framework/permissions.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
from django.core.exceptions import ImproperlyConfigured
4+
from rest_framework.exceptions import PermissionDenied
45
from rest_framework.permissions import BasePermission, IsAuthenticated, SAFE_METHODS
56

67
from ...settings import oauth2_settings
@@ -25,7 +26,24 @@ def has_permission(self, request, view):
2526
required_scopes = self.get_scopes(request, view)
2627
log.debug("Required scopes to access resource: {0}".format(required_scopes))
2728

28-
return token.is_valid(required_scopes)
29+
if token.is_valid(required_scopes):
30+
return True
31+
32+
# Provide information about required scope?
33+
include_required_scope = (
34+
oauth2_settings.ERROR_RESPONSE_WITH_SCOPES and
35+
required_scopes and
36+
not token.is_expired() and
37+
not token.allow_scopes(required_scopes)
38+
)
39+
40+
if include_required_scope:
41+
self.message = {
42+
"detail": PermissionDenied.default_detail,
43+
"required_scopes": list(required_scopes),
44+
}
45+
46+
return False
2947

3048
assert False, ("TokenHasScope requires the"
3149
"`oauth2_provider.rest_framework.OAuth2Authentication` authentication "

oauth2_provider/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"ACCESS_TOKEN_EXPIRE_SECONDS": 36000,
4747
"REFRESH_TOKEN_EXPIRE_SECONDS": None,
4848
"ROTATE_REFRESH_TOKEN": True,
49+
"ERROR_RESPONSE_WITH_SCOPES": False,
4950
"APPLICATION_MODEL": APPLICATION_MODEL,
5051
"ACCESS_TOKEN_MODEL": ACCESS_TOKEN_MODEL,
5152
"GRANT_MODEL": GRANT_MODEL,

tests/test_rest_framework.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
from oauth2_provider.settings import oauth2_settings
1313

1414

15+
try:
16+
from unittest import mock
17+
except ImportError:
18+
import mock
19+
20+
1521
Application = get_application_model()
1622
AccessToken = get_access_token_model()
1723
UserModel = get_user_model()
@@ -243,3 +249,24 @@ def test_resource_scoped_permission_post_denied(self):
243249
auth = self._create_authorization_header(self.access_token.token)
244250
response = self.client.post("/oauth2-resource-scoped-test/", HTTP_AUTHORIZATION=auth)
245251
self.assertEqual(response.status_code, 403)
252+
253+
@unittest.skipUnless(rest_framework_installed, "djangorestframework not installed")
254+
@mock.patch.object(oauth2_settings, "ERROR_RESPONSE_WITH_SCOPES", new=True)
255+
def test_required_scope_in_response(self):
256+
self.access_token.scope = "scope2"
257+
self.access_token.save()
258+
259+
auth = self._create_authorization_header(self.access_token.token)
260+
response = self.client.get("/oauth2-scoped-test/", HTTP_AUTHORIZATION=auth)
261+
self.assertEqual(response.status_code, 403)
262+
self.assertEqual(response.data["required_scopes"], ["scope1"])
263+
264+
@unittest.skipUnless(rest_framework_installed, "djangorestframework not installed")
265+
def test_required_scope_not_in_response_by_default(self):
266+
self.access_token.scope = "scope2"
267+
self.access_token.save()
268+
269+
auth = self._create_authorization_header(self.access_token.token)
270+
response = self.client.get("/oauth2-scoped-test/", HTTP_AUTHORIZATION=auth)
271+
self.assertEqual(response.status_code, 403)
272+
self.assertNotIn("required_scopes", response.data)

0 commit comments

Comments
 (0)