diff --git a/edx_rest_framework_extensions/__init__.py b/edx_rest_framework_extensions/__init__.py index 30187b4d..1321c330 100644 --- a/edx_rest_framework_extensions/__init__.py +++ b/edx_rest_framework_extensions/__init__.py @@ -1,3 +1,3 @@ """ edx Django REST Framework extensions. """ -__version__ = '4.0.3' # pragma: no cover +__version__ = '4.0.4' # pragma: no cover diff --git a/edx_rest_framework_extensions/auth/jwt/authentication.py b/edx_rest_framework_extensions/auth/jwt/authentication.py index afcd5901..e3c25ac4 100644 --- a/edx_rest_framework_extensions/auth/jwt/authentication.py +++ b/edx_rest_framework_extensions/auth/jwt/authentication.py @@ -9,7 +9,7 @@ from rest_framework_jwt.authentication import JSONWebTokenAuthentication from edx_rest_framework_extensions.auth.jwt.constants import USE_JWT_COOKIE_HEADER -from edx_rest_framework_extensions.auth.jwt.decoder import jwt_decode_handler +from edx_rest_framework_extensions.auth.jwt.decoder import configured_jwt_decode_handler from edx_rest_framework_extensions.settings import get_setting @@ -187,4 +187,4 @@ def get_decoded_jwt_from_auth(request): if not is_jwt_authenticated(request): return None - return jwt_decode_handler(request.auth) + return configured_jwt_decode_handler(request.auth) diff --git a/edx_rest_framework_extensions/auth/jwt/cookies.py b/edx_rest_framework_extensions/auth/jwt/cookies.py index 0c13e308..4866498e 100644 --- a/edx_rest_framework_extensions/auth/jwt/cookies.py +++ b/edx_rest_framework_extensions/auth/jwt/cookies.py @@ -4,7 +4,7 @@ from django.conf import settings -from edx_rest_framework_extensions.auth.jwt.decoder import jwt_decode_handler +from edx_rest_framework_extensions.auth.jwt.decoder import configured_jwt_decode_handler def jwt_cookie_name(): @@ -30,4 +30,4 @@ def get_decoded_jwt(request): if not jwt_cookie: return None - return jwt_decode_handler(jwt_cookie) + return configured_jwt_decode_handler(jwt_cookie) diff --git a/edx_rest_framework_extensions/auth/jwt/decoder.py b/edx_rest_framework_extensions/auth/jwt/decoder.py index e9b57dea..6ed063b9 100644 --- a/edx_rest_framework_extensions/auth/jwt/decoder.py +++ b/edx_rest_framework_extensions/auth/jwt/decoder.py @@ -44,6 +44,10 @@ def jwt_decode_handler(token): 'JWT_PUBLIC_SIGNING_JWK_SET': 'the-jwk-set-of-public-signing-keys', } + Warning: + Do **not** use this method internally. Only use it in ``JWT_DECODE_HANDLER`` like the above example. + Internally, use ``configured_jwt_decode_handler`` which respects the ``JWT_DECODE_HANDLER`` setting. + Args: token (str): JWT to be decoded. @@ -60,18 +64,26 @@ def jwt_decode_handler(token): return _set_token_defaults(decoded_token) +def configured_jwt_decode_handler(token): + """ + Calls the ``jwt_decode_handler`` configured in the ``JWT_DECODE_HANDLER`` setting. + """ + api_setting_jwt_decode_handler = api_settings.JWT_DECODE_HANDLER + return api_setting_jwt_decode_handler(token) + + def decode_jwt_scopes(token): """ Decode the JWT and return the scopes claim. """ - return jwt_decode_handler(token).get('scopes', []) + return configured_jwt_decode_handler(token).get('scopes', []) def decode_jwt_is_restricted(token): """ Decode the JWT and return the is_restricted claim. """ - return jwt_decode_handler(token)['is_restricted'] + return configured_jwt_decode_handler(token).get('is_restricted', False) def decode_jwt_filters(token): @@ -79,7 +91,10 @@ def decode_jwt_filters(token): Decode the JWT, parse the filters claim, and return a list of (filter_type, filter_value) tuples. """ - return [jwt_filter.split(':') for jwt_filter in jwt_decode_handler(token)['filters']] + return [ + jwt_filter.split(':') + for jwt_filter in configured_jwt_decode_handler(token).get('filters', []) + ] def _set_token_defaults(token): diff --git a/edx_rest_framework_extensions/auth/jwt/tests/test_decoder.py b/edx_rest_framework_extensions/auth/jwt/tests/test_decoder.py index f718fab9..94ecb0dc 100644 --- a/edx_rest_framework_extensions/auth/jwt/tests/test_decoder.py +++ b/edx_rest_framework_extensions/auth/jwt/tests/test_decoder.py @@ -7,7 +7,12 @@ from django.conf import settings from django.test import TestCase, override_settings -from edx_rest_framework_extensions.auth.jwt.decoder import jwt_decode_handler +from edx_rest_framework_extensions.auth.jwt.decoder import ( + decode_jwt_filters, + decode_jwt_is_restricted, + decode_jwt_scopes, + jwt_decode_handler, +) from edx_rest_framework_extensions.auth.jwt.tests.utils import ( generate_jwt_token, generate_latest_version_payload, @@ -137,3 +142,66 @@ def test_upgrade(self): # Keep time-related values constant for full-proof comparison. upgraded_payload['iat'], upgraded_payload['exp'] = jwt_payload['iat'], jwt_payload['exp'] self.assertDictEqual(jwt_decode_handler(token), upgraded_payload) + + +def _jwt_decode_handler_with_defaults(token): # pylint: disable=unused-argument + """ + Accepts anything as a token and returns a fake JWT payload with defaults. + """ + return { + 'scopes': ['fake:scope'], + 'is_restricted': True, + 'filters': ['fake:filter'], + } + + +def _jwt_decode_handler_no_defaults(token): # pylint: disable=unused-argument + """ + Accepts anything as a token and returns a fake JWT payload with no defaults. + """ + return {} + + +@ddt.ddt +class JWTDecodeHandlerSettingTests(TestCase): + """ + Tests to ensure utility functions respect JWT_DECODE_HANDLER setting. + + Note: An attempt was made to use ``override_settings`` to actually set + ``JWT_DECODE_HANDLER``, but clean-up of the tests in tearDown was not working, + even after reloading the module, and it was failing other tests in the test suite. + """ + NORMALLY_INVALID_TOKEN = 'this is a valid jwt only with fake_jwt_decode_handler' + + @ddt.data( + ('_jwt_decode_handler_with_defaults', ['fake:scope']), + ('_jwt_decode_handler_no_defaults', []) + ) + @ddt.unpack + @mock.patch('edx_rest_framework_extensions.auth.jwt.decoder.api_settings') + def test_decode_jwt_scopes(self, jwt_decode_handler_name, expected_scope, mock_api_settings): + mock_api_settings.JWT_DECODE_HANDLER = globals()[jwt_decode_handler_name] + scopes = decode_jwt_scopes(self.NORMALLY_INVALID_TOKEN) + self.assertEqual(scopes, expected_scope) + + @ddt.data( + ('_jwt_decode_handler_with_defaults', True), + ('_jwt_decode_handler_no_defaults', False) + ) + @ddt.unpack + @mock.patch('edx_rest_framework_extensions.auth.jwt.decoder.api_settings') + def test_decode_jwt_is_restricted(self, jwt_decode_handler_name, expected_is_restricted, mock_api_settings): + mock_api_settings.JWT_DECODE_HANDLER = globals()[jwt_decode_handler_name] + is_restricted = decode_jwt_is_restricted(self.NORMALLY_INVALID_TOKEN) + self.assertEqual(is_restricted, expected_is_restricted) + + @ddt.data( + ('_jwt_decode_handler_with_defaults', [['fake', 'filter']]), + ('_jwt_decode_handler_no_defaults', []) + ) + @ddt.unpack + @mock.patch('edx_rest_framework_extensions.auth.jwt.decoder.api_settings') + def test_decode_jwt_filters(self, jwt_decode_handler_name, expected_filter, mock_api_settings): + mock_api_settings.JWT_DECODE_HANDLER = globals()[jwt_decode_handler_name] + filters = decode_jwt_filters(self.NORMALLY_INVALID_TOKEN) + self.assertEqual(filters, expected_filter)