diff --git a/.travis.yml b/.travis.yml index 4d5ba8a5..f09ac530 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,15 @@ python: - 3.5 env: - - TOXENV=django111-drf37 AFTER_SUCCESS=codecov INSTALL_EXTRAS=codecov - - TOXENV=django111-drf38 + - TOXENV=django111-drf39 AFTER_SUCCESS=codecov INSTALL_EXTRAS=codecov - TOXENV=django111-drf39 - TOXENV=django111-drflatest + - TOXENV=django20-drf39 + - TOXENV=django20-drflatest + - TOXENV=django21-drf39 + - TOXENV=django21-drflatest + - TOXENV=django22-drf39 + - TOXENV=django22-drflatest matrix: include: diff --git a/edx_rest_framework_extensions/__init__.py b/edx_rest_framework_extensions/__init__.py index 12e91c97..94100825 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.0' # pragma: no cover +__version__ = '4.0.1' # pragma: no cover diff --git a/edx_rest_framework_extensions/auth/jwt/authentication.py b/edx_rest_framework_extensions/auth/jwt/authentication.py index 6081b84d..afcd5901 100644 --- a/edx_rest_framework_extensions/auth/jwt/authentication.py +++ b/edx_rest_framework_extensions/auth/jwt/authentication.py @@ -2,13 +2,11 @@ import logging +import jwt from django.contrib.auth import get_user_model from django.middleware.csrf import CsrfViewMiddleware from rest_framework import exceptions -from rest_framework_jwt.authentication import ( - BaseJSONWebTokenAuthentication, - JSONWebTokenAuthentication, -) +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 @@ -78,6 +76,8 @@ def authenticate(self, request): # CSRF passed validation with authenticated user return user_and_auth + except jwt.InvalidTokenError: + raise exceptions.AuthenticationFailed() except Exception as ex: # Errors in production do not need to be logged (as they may be noisy), # but debug logging can help quickly resolve issues during development. @@ -166,7 +166,7 @@ def enforce_csrf(self, request): def is_jwt_authenticated(request): successful_authenticator = getattr(request, 'successful_authenticator', None) - if not isinstance(successful_authenticator, BaseJSONWebTokenAuthentication): + if not isinstance(successful_authenticator, JSONWebTokenAuthentication): return False if not getattr(request, 'auth', None): logger.error( diff --git a/edx_rest_framework_extensions/auth/jwt/middleware.py b/edx_rest_framework_extensions/auth/jwt/middleware.py index 587a8d14..f01a0b32 100644 --- a/edx_rest_framework_extensions/auth/jwt/middleware.py +++ b/edx_rest_framework_extensions/auth/jwt/middleware.py @@ -10,7 +10,7 @@ from edx_django_utils import monitoring from edx_django_utils.cache import RequestCache from rest_framework.request import Request -from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication +from rest_framework_jwt.authentication import JSONWebTokenAuthentication from edx_rest_framework_extensions.auth.jwt.constants import ( JWT_DELIMITER, @@ -80,7 +80,7 @@ def process_view(self, request, view_func, view_args, view_kwargs): # pylint: d view_class = _get_view_class(view_func) view_authentication_classes = getattr(view_class, 'authentication_classes', tuple()) - if _includes_base_class(view_authentication_classes, BaseJSONWebTokenAuthentication): + if _includes_base_class(view_authentication_classes, JSONWebTokenAuthentication): self._add_missing_jwt_permission_classes(view_class) @@ -184,7 +184,7 @@ class JwtAuthCookieMiddleware(MiddlewareMixin): This middleware must appear before any AuthenticationMiddleware. For example:: - MIDDLEWARE_CLASSES = ( + MIDDLEWARE = ( 'edx_rest_framework_extensions.auth.jwt.middleware.JwtAuthCookieMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ) @@ -205,7 +205,7 @@ def process_view(self, request, view_func, view_args, view_kwargs): # pylint: d """ Reconstitute the full JWT and add a new cookie on the request object. """ - assert hasattr(request, 'session'), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." # noqa E501 line too long + assert hasattr(request, 'session'), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." # noqa E501 line too long use_jwt_cookie_requested = request.META.get(USE_JWT_COOKIE_HEADER) header_payload_cookie = request.COOKIES.get(jwt_cookie_header_payload_name()) @@ -248,7 +248,7 @@ def process_view(self, request, view_func, view_args, view_kwargs): # pylint: d def _get_user_from_jwt(request, view_func): user = get_user(request) - if user.is_authenticated(): + if user.is_authenticated: return user try: @@ -271,14 +271,14 @@ def _get_user_from_jwt(request, view_func): def _get_jwt_authentication_class(view_func): """ - Returns the first DRF Authentication class that is a subclass of BaseJSONWebTokenAuthentication + Returns the first DRF Authentication class that is a subclass of JSONWebTokenAuthentication """ view_class = _get_view_class(view_func) view_authentication_classes = getattr(view_class, 'authentication_classes', tuple()) - if _includes_base_class(view_authentication_classes, BaseJSONWebTokenAuthentication): + if _includes_base_class(view_authentication_classes, JSONWebTokenAuthentication): return next( current_class for current_class in view_authentication_classes - if issubclass(current_class, BaseJSONWebTokenAuthentication) + if issubclass(current_class, JSONWebTokenAuthentication) ) return None diff --git a/edx_rest_framework_extensions/auth/jwt/tests/test_middleware.py b/edx_rest_framework_extensions/auth/jwt/tests/test_middleware.py index 5cebbf31..e5a61449 100644 --- a/edx_rest_framework_extensions/auth/jwt/tests/test_middleware.py +++ b/edx_rest_framework_extensions/auth/jwt/tests/test_middleware.py @@ -17,7 +17,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import ViewSet -from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication +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.cookies import ( @@ -45,7 +45,7 @@ class SomeIncludedPermissionClass: pass -class SomeJwtAuthenticationSubclass(BaseJSONWebTokenAuthentication): +class SomeJwtAuthenticationSubclass(JSONWebTokenAuthentication): pass @@ -183,7 +183,7 @@ class HasNoCondPermView(APIView): self.assertIn(NotJwtRestrictedApplication, HasNoCondPermView.permission_classes) -class MockJwtAuthentication(BaseJSONWebTokenAuthentication): +class MockJwtAuthentication(JSONWebTokenAuthentication): """ Authenticates a user if the reconstituted jwt cookie contains the expected value. @@ -276,15 +276,15 @@ def setUp(self): ('/loginredirectifunauthenticated/', True, 200), ('/isauthenticatedandloginredirect/', False, 302), ('/isauthenticatedandloginredirect/', True, 200), - ('/isauthenticated/', False, 403), - ('/isauthenticated/', True, 403), + ('/isauthenticated/', False, 401), + ('/isauthenticated/', True, 401), ('/nopermissionsrequired/', False, 200), ('/nopermissionsrequired/', True, 200), ) @ddt.unpack @override_settings( ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_middleware', - MIDDLEWARE_CLASSES=( + MIDDLEWARE=( 'django.contrib.sessions.middleware.SessionMiddleware', 'edx_rest_framework_extensions.auth.jwt.middleware.JwtRedirectToLoginIfUnauthenticatedMiddleware', 'edx_rest_framework_extensions.auth.jwt.middleware.JwtAuthCookieMiddleware', @@ -304,15 +304,15 @@ def test_login_required_middleware(self, url, has_jwt_cookies, expected_status): ('/loginredirectifunauthenticated/', True, 302), ('/isauthenticatedandloginredirect/', False, 302), ('/isauthenticatedandloginredirect/', True, 302), - ('/isauthenticated/', False, 403), - ('/isauthenticated/', True, 403), + ('/isauthenticated/', False, 401), + ('/isauthenticated/', True, 401), ('/nopermissionsrequired/', False, 200), ('/nopermissionsrequired/', True, 200), ) @ddt.unpack @override_settings( ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_middleware', - MIDDLEWARE_CLASSES=( + MIDDLEWARE=( 'django.contrib.sessions.middleware.SessionMiddleware', 'edx_rest_framework_extensions.auth.jwt.tests.test_middleware.OverriddenJwtRedirectToLoginIfUnauthenticatedMiddleware', # noqa E501 line too long 'edx_rest_framework_extensions.auth.jwt.middleware.JwtAuthCookieMiddleware', @@ -334,7 +334,7 @@ class CheckRequestUserForJwtAuthMiddleware(MiddlewareMixin): set the request.user. """ def process_view(self, request, view_func, view_args, view_kwargs): # pylint: disable=unused-argument - assert request.user.is_authenticated(), 'Request.user was expected to be authenticated.' + assert request.user.is_authenticated, 'Request.user was expected to be authenticated.' class CheckRequestUserAnonymousForJwtAuthMiddleware(MiddlewareMixin): @@ -343,7 +343,7 @@ class CheckRequestUserAnonymousForJwtAuthMiddleware(MiddlewareMixin): the user (e.g. a failed cookie). """ def process_view(self, request, view_func, view_args, view_kwargs): # pylint: disable=unused-argument - assert not request.user.is_authenticated(), 'Request.user was expected to be anonymous.' + assert not request.user.is_authenticated, 'Request.user was expected to be anonymous.' @ddt.ddt @@ -422,7 +422,7 @@ def test_set_request_user_with_use_jwt_cookie( ) with override_settings( ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_middleware', - MIDDLEWARE_CLASSES=( + MIDDLEWARE=( 'django.contrib.sessions.middleware.SessionMiddleware', 'edx_rest_framework_extensions.auth.jwt.middleware.JwtAuthCookieMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', diff --git a/edx_rest_framework_extensions/middleware.py b/edx_rest_framework_extensions/middleware.py index 800a421c..a38bb4d9 100644 --- a/edx_rest_framework_extensions/middleware.py +++ b/edx_rest_framework_extensions/middleware.py @@ -20,7 +20,7 @@ class RequestMetricsMiddleware(MiddlewareMixin): This middleware is dependent on the RequestCacheMiddleware. You must include this middleware later. For example:: - MIDDLEWARE_CLASSES = ( + MIDDLEWARE = ( 'edx_django_utils.cache.middleware.RequestCacheMiddleware', 'edx_rest_framework_extensions.middleware.RequestMetricsMiddleware', ) diff --git a/edx_rest_framework_extensions/tests/test_permissions.py b/edx_rest_framework_extensions/tests/test_permissions.py index 172f9819..e009bd9d 100644 --- a/edx_rest_framework_extensions/tests/test_permissions.py +++ b/edx_rest_framework_extensions/tests/test_permissions.py @@ -10,7 +10,7 @@ from rest_framework.authentication import SessionAuthentication from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication +from rest_framework_jwt.authentication import JSONWebTokenAuthentication from edx_rest_framework_extensions import permissions from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication @@ -105,7 +105,7 @@ class JwtApplicationPermissionsTests(TestCase): @ddt.data( *product( (permissions.JwtRestrictedApplication, permissions.NotJwtRestrictedApplication), - (JwtAuthentication, BaseJSONWebTokenAuthentication, SessionAuthentication, None), + (JwtAuthentication, JSONWebTokenAuthentication, SessionAuthentication, None), (True, False), ) ) @@ -116,7 +116,7 @@ def test_has_permission(self, permission_class, authentication_class, is_restric request.user = factories.UserFactory() request.auth = generate_jwt(request.user, is_restricted=is_restricted) - is_jwt_auth_subclass = issubclass(type(request.successful_authenticator), BaseJSONWebTokenAuthentication) + is_jwt_auth_subclass = issubclass(type(request.successful_authenticator), JSONWebTokenAuthentication) has_permission = permission_class().has_permission(request, view=None) expected_restricted_permission = is_restricted and is_jwt_auth_subclass diff --git a/requirements/base.in b/requirements/base.in index 16fc77cb..f257a031 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -2,11 +2,11 @@ Django>=1.11 djangorestframework>=3.2.0,<3.10 -djangorestframework-jwt>=1.7.2,<2.0.0 +drf-jwt django-waffle edx-django-utils edx-opaque-keys -pyjwkest==1.3.2 +pyjwkest python-dateutil>=2.0 requests>=2.7.0 rest-condition>=1.0.3 diff --git a/requirements/base.txt b/requirements/base.txt index 823134aa..46015675 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -7,9 +7,9 @@ certifi==2019.11.28 # via requests chardet==3.0.4 # via requests django-waffle==0.18.0 # via -c requirements/constraints.txt, -r requirements/base.in, edx-django-utils -django==1.11.29 # via -c requirements/constraints.txt, -r requirements/base.in, edx-django-utils, rest-condition -djangorestframework-jwt==1.11.0 # via -r requirements/base.in -djangorestframework==3.9.4 # via -r requirements/base.in, rest-condition +django==1.11.29 # via -c requirements/constraints.txt, -r requirements/base.in, drf-jwt, edx-django-utils, rest-condition +djangorestframework==3.9.4 # via -r requirements/base.in, drf-jwt, rest-condition +drf-jwt==1.14.0 # via -r requirements/base.in edx-django-utils==3.0 # via -r requirements/base.in edx-opaque-keys==2.0.1 # via -r requirements/base.in future==0.18.2 # via pyjwkest @@ -18,8 +18,8 @@ newrelic==5.8.0.136 # via edx-django-utils pbr==5.4.4 # via stevedore psutil==1.2.1 # via edx-django-utils pycryptodomex==3.9.7 # via pyjwkest -pyjwkest==1.3.2 # via -r requirements/base.in -pyjwt==1.7.1 # via djangorestframework-jwt +pyjwkest==1.4.2 # via -r requirements/base.in +pyjwt==1.7.1 # via drf-jwt pymongo==3.10.1 # via edx-opaque-keys python-dateutil==2.8.1 # via -r requirements/base.in pytz==2019.3 # via django diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 90482bbd..486355c4 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -12,4 +12,7 @@ django<2.0 # django-waffle 0.19.0 dropped support for Django 2.0 and 2.1. -django-waffle<0.19.0 \ No newline at end of file +django-waffle<0.19.0 + +# django22 tests are failing. +drf-jwt<1.15.0 \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index be0e3f26..6adf0ee1 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -17,10 +17,10 @@ coverage==4.5.4 # via -r requirements/test.txt, pytest-cov ddt==1.2.2 # via -r requirements/test.txt distlib==0.3.0 # via -r requirements/test.txt, virtualenv django-waffle==0.18.0 # via -c requirements/constraints.txt, -r requirements/base.txt, -r requirements/test.txt, edx-django-utils -django==1.11.29 # via -c requirements/constraints.txt, -r requirements/base.txt, -r requirements/test.txt, edx-django-utils, rest-condition -djangorestframework-jwt==1.11.0 # via -r requirements/base.txt, -r requirements/test.txt -djangorestframework==3.9.4 # via -r requirements/base.txt, -r requirements/test.txt, rest-condition +django==1.11.29 # via -c requirements/constraints.txt, -r requirements/base.txt, -r requirements/test.txt, drf-jwt, edx-django-utils, rest-condition +djangorestframework==3.9.4 # via -r requirements/base.txt, -r requirements/test.txt, drf-jwt, rest-condition docutils==0.16 # via -r requirements/docs.txt, sphinx +drf-jwt==1.14.0 # via -r requirements/base.txt, -r requirements/test.txt edx-django-utils==3.0 # via -r requirements/base.txt, -r requirements/test.txt edx-lint==1.4.1 # via -r requirements/test.txt edx-opaque-keys==2.0.1 # via -r requirements/base.txt, -r requirements/test.txt @@ -50,8 +50,8 @@ py==1.8.1 # via -r requirements/test.txt, pytest, tox pycodestyle==2.5.0 # via -r requirements/test.txt pycryptodomex==3.9.7 # via -r requirements/base.txt, -r requirements/test.txt, pyjwkest pygments==2.5.2 # via -r requirements/docs.txt, sphinx -pyjwkest==1.3.2 # via -r requirements/base.txt, -r requirements/test.txt -pyjwt==1.7.1 # via -r requirements/base.txt, -r requirements/test.txt, djangorestframework-jwt +pyjwkest==1.4.2 # via -r requirements/base.txt, -r requirements/test.txt +pyjwt==1.7.1 # via -r requirements/base.txt, -r requirements/test.txt, drf-jwt pylint-celery==0.3 # via -r requirements/test.txt, edx-lint pylint-django==2.0.11 # via -r requirements/test.txt, edx-lint pylint-plugin-utils==0.6 # via -r requirements/test.txt, pylint-celery, pylint-django @@ -69,7 +69,7 @@ semantic-version==2.8.4 # via -r requirements/base.txt, -r requirements/test.t six==1.14.0 # via -r requirements/base.txt, -r requirements/docs.txt, -r requirements/test.txt, astroid, edx-lint, edx-opaque-keys, httpretty, mock, packaging, pathlib2, pyjwkest, python-dateutil, stevedore, tox, virtualenv snowballstemmer==2.0.0 # via -r requirements/docs.txt, sphinx sphinx-rtd-theme==0.4.3 # via -r requirements/docs.txt -sphinx==2.4.3 # via -r requirements/docs.txt, sphinx-rtd-theme +sphinx==2.4.4 # via -r requirements/docs.txt, sphinx-rtd-theme sphinxcontrib-applehelp==1.0.2 # via -r requirements/docs.txt, sphinx sphinxcontrib-devhelp==1.0.2 # via -r requirements/docs.txt, sphinx sphinxcontrib-htmlhelp==1.0.3 # via -r requirements/docs.txt, sphinx diff --git a/requirements/docs.txt b/requirements/docs.txt index fff4dbe3..b2c86ad1 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -21,7 +21,7 @@ requests==2.23.0 # via -c requirements/test.txt, sphinx six==1.14.0 # via -c requirements/test.txt, packaging snowballstemmer==2.0.0 # via sphinx sphinx-rtd-theme==0.4.3 # via -r requirements/docs.in -sphinx==2.4.3 # via -r requirements/docs.in, sphinx-rtd-theme +sphinx==2.4.4 # via -r requirements/docs.in, sphinx-rtd-theme sphinxcontrib-applehelp==1.0.2 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx sphinxcontrib-htmlhelp==1.0.3 # via sphinx diff --git a/requirements/test.in b/requirements/test.in index 3d8abae3..42e544d5 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -16,4 +16,3 @@ pycodestyle pytest-cov pytest-django tox>=2.3.1,<3.0.0 -pyjwkest==1.3.2 diff --git a/requirements/test.txt b/requirements/test.txt index b4d2f7cb..18fd1a08 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -15,7 +15,7 @@ coverage==4.5.4 # via -r requirements/test.in, pytest-cov ddt==1.2.2 # via -r requirements/test.in distlib==0.3.0 # via virtualenv django-waffle==0.18.0 # via -c requirements/constraints.txt, -r requirements/base.txt, edx-django-utils -djangorestframework-jwt==1.11.0 # via -r requirements/base.txt +drf-jwt==1.14.0 # via -r requirements/base.txt edx-django-utils==3.0 # via -r requirements/base.txt edx-lint==1.4.1 # via -r requirements/test.in edx-opaque-keys==2.0.1 # via -r requirements/base.txt @@ -41,8 +41,8 @@ psutil==1.2.1 # via -r requirements/base.txt, edx-django-utils py==1.8.1 # via pytest, tox pycodestyle==2.5.0 # via -r requirements/test.in pycryptodomex==3.9.7 # via -r requirements/base.txt, pyjwkest -pyjwkest==1.3.2 # via -r requirements/base.txt, -r requirements/test.in -pyjwt==1.7.1 # via -r requirements/base.txt, djangorestframework-jwt +pyjwkest==1.4.2 # via -r requirements/base.txt +pyjwt==1.7.1 # via -r requirements/base.txt, drf-jwt pylint-celery==0.3 # via edx-lint pylint-django==2.0.11 # via edx-lint pylint-plugin-utils==0.6 # via pylint-celery, pylint-django diff --git a/test_settings.py b/test_settings.py index 1e5e2edc..771bb8cf 100644 --- a/test_settings.py +++ b/test_settings.py @@ -14,6 +14,7 @@ 'django.contrib.auth', 'django.contrib.contenttypes', 'edx_rest_framework_extensions', + 'rest_framework_jwt', 'waffle', ) @@ -50,6 +51,7 @@ 'JWT_VERIFY_EXPIRATION': True, + 'JWT_AUTH_HEADER_PREFIX': 'JWT', # JWT_ISSUERS enables token decoding for multiple issuers (Note: This is not a native DRF-JWT field) # We use it to allow different values for the 'ISSUER' field, but keep the same SECRET_KEY and # AUDIENCE values across all issuers. diff --git a/tox.ini b/tox.ini index a819144f..a715b29b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35-django{111,20,21,22}-drf{37,38,39,latest},quality, docs +envlist = py35-django{111,20,21,22}-drf{39,latest},quality, docs [testenv] setenv =