From e3ac7999031ea55590b8a8a1c38149cf082d5d0c Mon Sep 17 00:00:00 2001 From: Robert Raposa Date: Tue, 10 Oct 2023 11:11:54 -0400 Subject: [PATCH] feat: handle jwt cookie vs session user mismatch (#388) Adds toggle EDX_DRF_EXTENSIONS[ENABLE_JWT_VS_SESSION_USER_MONITORING] to enable the following features: - New custom attributes is_jwt_vs_session_user_check_enabled, jwt_auth_session_user_id, jwt_auth_and_session_user_mismatch, and invalid_jwt_cookie_user_id for monitoring and debugging. - When forgiving JWT cookies are also enabled, user mismatches will now result in a failure, rather than a forgiving JWT. --- CHANGELOG.rst | 10 + edx_rest_framework_extensions/__init__.py | 2 +- .../auth/jwt/authentication.py | 125 +++++++++- .../auth/jwt/decoder.py | 31 +++ .../auth/jwt/tests/test_authentication.py | 215 +++++++++++++++++- .../auth/jwt/tests/test_decoder.py | 15 ++ .../auth/jwt/tests/utils.py | 1 + edx_rest_framework_extensions/config.py | 14 ++ edx_rest_framework_extensions/settings.py | 2 + test_settings.py | 1 + 10 files changed, 402 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 741fd539..547c97d0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,16 @@ Change Log Unreleased ---------- +[8.11.0] - 2023-10-04 +--------------------- + +Added +~~~~~ +* Added toggle EDX_DRF_EXTENSIONS[ENABLE_JWT_VS_SESSION_USER_CHECK] to enable the following: + + * New custom attributes is_jwt_vs_session_user_check_enabled, jwt_auth_session_user_id, jwt_auth_and_session_user_mismatch, and invalid_jwt_cookie_user_id for monitoring and debugging. + * When forgiving JWT cookies are also enabled, user mismatches will now result in a failure, rather than a forgiving JWT. + [8.10.0] - 2023-09-19 --------------------- diff --git a/edx_rest_framework_extensions/__init__.py b/edx_rest_framework_extensions/__init__.py index 3066d7b6..b38d3487 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__ = '8.10.0' # pragma: no cover +__version__ = '8.11.0' # pragma: no cover diff --git a/edx_rest_framework_extensions/auth/jwt/authentication.py b/edx_rest_framework_extensions/auth/jwt/authentication.py index 5615d0eb..3ac22bea 100644 --- a/edx_rest_framework_extensions/auth/jwt/authentication.py +++ b/edx_rest_framework_extensions/auth/jwt/authentication.py @@ -9,8 +9,14 @@ from rest_framework import exceptions from rest_framework_jwt.authentication import JSONWebTokenAuthentication -from edx_rest_framework_extensions.auth.jwt.decoder import configured_jwt_decode_handler -from edx_rest_framework_extensions.config import ENABLE_FORGIVING_JWT_COOKIES +from edx_rest_framework_extensions.auth.jwt.decoder import ( + configured_jwt_decode_handler, + unsafe_jwt_decode_handler, +) +from edx_rest_framework_extensions.config import ( + ENABLE_FORGIVING_JWT_COOKIES, + ENABLE_JWT_VS_SESSION_USER_CHECK, +) from edx_rest_framework_extensions.settings import get_setting @@ -100,6 +106,8 @@ def authenticate(self, request): # CSRF passed validation with authenticated user set_custom_attribute('jwt_auth_result', 'success-cookie') + # adds additional monitoring for mismatches + self._monitor_successful_jwt_cookie_and_session_user_mismatch(request, jwt_user_id=user_and_auth[0].id) return user_and_auth except Exception as exception: @@ -112,14 +120,19 @@ def authenticate(self, request): exception_to_report = _deepest_jwt_exception(exception) set_custom_attribute('jwt_auth_failed', 'Exception:{}'.format(repr(exception_to_report))) - is_jwt_failure_forgiven = is_forgiving_jwt_cookies_enabled and is_authenticating_with_jwt_cookie - if is_jwt_failure_forgiven: - set_custom_attribute('jwt_auth_result', 'forgiven-failure') - return None if is_authenticating_with_jwt_cookie: + # This check also adds monitoring details for all failed JWT cookies + is_user_mismatch = self._is_failed_jwt_cookie_and_session_user_mismatch(request) + if is_forgiving_jwt_cookies_enabled: + if is_user_mismatch: + set_custom_attribute('jwt_auth_result', 'user-mismatch-failure') + raise + set_custom_attribute('jwt_auth_result', 'forgiven-failure') + return None set_custom_attribute('jwt_auth_result', 'failed-cookie') - else: - set_custom_attribute('jwt_auth_result', 'failed-auth-header') + raise + + set_custom_attribute('jwt_auth_result', 'failed-auth-header') raise def authenticate_credentials(self, payload): @@ -216,6 +229,102 @@ def is_authenticating_with_jwt_cookie(cls, request): except Exception: # pylint: disable=broad-exception-caught return False + def _is_failed_jwt_cookie_and_session_user_mismatch(self, request): + """ + Returns True if failed JWT cookie and session user do not match, False otherwise. + + Notes: + - Must only be called in the case of a JWT cookie failure. + - Also provides monitoring details for mismatches. + """ + try: + cookie_token = JSONWebTokenAuthentication.get_token_from_cookies(request.COOKIES) + invalid_decoded_jwt = unsafe_jwt_decode_handler(cookie_token) + jwt_user_id = invalid_decoded_jwt.get('user_id', None) + jwt_user_id_attribute_value = jwt_user_id if jwt_user_id else 'not-found' # pragma: no cover + except Exception: # pylint: disable=broad-exception-caught + jwt_user_id = None + jwt_user_id_attribute_value = 'decode-error' + + # .. custom_attribute_name: failed_jwt_cookie_user_id + # .. custom_attribute_description: The user_id pulled from the failed + # JWT cookie. If the user_id claim is not found in the JWT, the attribute + # value will be 'not-found'. If the failed JWT simply can't be decoded, + # the attribute value will be 'decode-error'. Note: for successful JWTs, + # the user id will already be available in `enduser.id` or `request_user_id`. + set_custom_attribute('failed_jwt_cookie_user_id', jwt_user_id_attribute_value) + + return self._is_jwt_cookie_and_session_user_mismatch(request, jwt_user_id) + + def _monitor_successful_jwt_cookie_and_session_user_mismatch(self, request, jwt_user_id): + """ + Provides monitoring when a successful JWT cookie and session user do not match. + + Notes: + - Must only be called in the case of a successful JWT cookie. + - Also provides monitoring details for mismatches. + """ + self._is_jwt_cookie_and_session_user_mismatch(request, jwt_user_id) + + def _is_jwt_cookie_and_session_user_mismatch(self, request, jwt_user_id): + """ + Returns True if JWT cookie and session user do not match, False otherwise. + + Arguments: + request: The request. + jwt_user_id (int): The user_id of the JWT, None if not found. + + Other notes: + - If ENABLE_JWT_VS_SESSION_USER_CHECK is toggled off, always return False. + - Also adds monitoring details for mismatches. + - Should only be called for JWT cookies. + """ + is_jwt_vs_session_user_check_enabled = get_setting(ENABLE_JWT_VS_SESSION_USER_CHECK) + # .. custom_attribute_name: is_jwt_vs_session_user_check_enabled + # .. custom_attribute_description: This is temporary custom attribute to show + # whether ENABLE_JWT_VS_SESSION_USER_CHECK is toggled on or off. + set_custom_attribute('is_jwt_vs_session_user_check_enabled', is_jwt_vs_session_user_check_enabled) + if not is_jwt_vs_session_user_check_enabled: + return False + + has_request_user = ( + hasattr(request, '_request') and hasattr(request._request, 'user') # pylint: disable=protected-access + ) + if not has_request_user: # pragma: no cover + # .. custom_attribute_name: jwt_auth_request_user_not_found + # .. custom_attribute_description: This custom attribute will show that we + # were unable to find the session user. This should not occur outside + # of tests, because there should still be an unauthenticated user, but + # this attribute could be used to check for the unexpected. + set_custom_attribute('jwt_auth_request_user_not_found', True) + return False + + wsgi_request_user = request._request.user # pylint: disable=protected-access + if wsgi_request_user and wsgi_request_user.is_authenticated: + session_user_id = wsgi_request_user.id + else: + session_user_id = None + + if not session_user_id or session_user_id == jwt_user_id: + return False + + # .. custom_attribute_name: jwt_auth_session_user_id + # .. custom_attribute_description: Session authentication may have completed + # in middleware before even getting to DRF. Although this authentication + # won't stick, because it will be replaced by DRF authentication, we + # record it, because it sometimes does not match the JWT cookie user. + # The name of this attribute is simply to clarify that this was found + # during JWT authentication. + set_custom_attribute('jwt_auth_session_user_id', session_user_id) + + # .. custom_attribute_name: jwt_auth_and_session_user_mismatch + # .. custom_attribute_description: True if session authentication user id and + # the JWT cookie user id may not match. When they match, this attribute + # won't be included. See jwt_auth_session_user_id for additional details. + set_custom_attribute('jwt_auth_and_session_user_mismatch', True) + + return True + def is_jwt_authenticated(request): successful_authenticator = getattr(request, 'successful_authenticator', None) diff --git a/edx_rest_framework_extensions/auth/jwt/decoder.py b/edx_rest_framework_extensions/auth/jwt/decoder.py index c9461296..79959263 100644 --- a/edx_rest_framework_extensions/auth/jwt/decoder.py +++ b/edx_rest_framework_extensions/auth/jwt/decoder.py @@ -74,6 +74,23 @@ def jwt_decode_handler(token, decode_symmetric_token=True): return _set_token_defaults(decoded_token) +def unsafe_jwt_decode_handler(token): + """ + Decodes a JSON Web Token (JWT) with NO verification. + + Args: + token (str): JWT to be decoded. + + Returns: + dict: Decoded JWT payload + + Raises: + InvalidTokenError: Decoding fails. + """ + decoded_token = _unsafe_decode_token_with_no_verification(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. @@ -361,6 +378,20 @@ def _decode_and_verify_token(token, jwt_issuer): return decoded_token +def _unsafe_decode_token_with_no_verification(token): + """ + Returns a decoded JWT token with no verification. + """ + options = { + 'verify_exp': False, + 'verify_aud': False, + 'verify_iss': False, + 'verify_signature': False, + } + decoded_token = jwt.decode(token, options=options) + return decoded_token + + def get_verification_jwk_key_set(asymmetric_keys=None, secret_key=None): """ Creates a JWK Keyset containing the provided keys. diff --git a/edx_rest_framework_extensions/auth/jwt/tests/test_authentication.py b/edx_rest_framework_extensions/auth/jwt/tests/test_authentication.py index 2b833b8f..061077a3 100644 --- a/edx_rest_framework_extensions/auth/jwt/tests/test_authentication.py +++ b/edx_rest_framework_extensions/auth/jwt/tests/test_authentication.py @@ -4,9 +4,15 @@ import ddt from django.contrib.auth import get_user_model +from django.http.cookie import SimpleCookie from django.test import RequestFactory, TestCase, override_settings +from django.urls import re_path as url_pattern +from django.urls import reverse from jwt import exceptions as jwt_exceptions from rest_framework.exceptions import AuthenticationFailed, PermissionDenied +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework_jwt.authentication import JSONWebTokenAuthentication from edx_rest_framework_extensions.auth.jwt import authentication @@ -18,7 +24,10 @@ generate_jwt_token, generate_latest_version_payload, ) -from edx_rest_framework_extensions.config import ENABLE_FORGIVING_JWT_COOKIES +from edx_rest_framework_extensions.config import ( + ENABLE_FORGIVING_JWT_COOKIES, + ENABLE_JWT_VS_SESSION_USER_CHECK, +) from edx_rest_framework_extensions.settings import get_setting from edx_rest_framework_extensions.tests import factories @@ -26,6 +35,23 @@ User = get_user_model() +class IsAuthenticatedView(APIView): + authentication_classes = (JwtAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request): # pylint: disable=unused-argument + return Response({'success': True}) + + +urlpatterns = [ + url_pattern( + r'^isauthenticated/$', + IsAuthenticatedView.as_view(), + name='authenticated-view', + ), +] + + @ddt.ddt class JwtAuthenticationTests(TestCase): """ JWT Authentication class tests. """ @@ -288,11 +314,190 @@ def test_authenticate_with_bearer_token(self, mock_set_custom_attribute): self.assertIsNone(JwtAuthentication().authenticate(request)) mock_set_custom_attribute.assert_any_call('jwt_auth_result', 'n/a') - def _get_test_jwt_token(self): + @override_settings( + EDX_DRF_EXTENSIONS={ENABLE_JWT_VS_SESSION_USER_CHECK: True}, + MIDDLEWARE=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + ), + ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_authentication', + ) + @mock.patch('edx_rest_framework_extensions.auth.jwt.authentication.set_custom_attribute') + def test_authenticate_jwt_and_session_mismatch(self, mock_set_custom_attribute): + """ Tests monitoring for JWT cookie when there is a session user mismatch """ + session_user_id = 111 + session_user = factories.UserFactory(id=session_user_id) + jwt_user = factories.UserFactory(id=222) + self.client.cookies = SimpleCookie({ + jwt_cookie_name(): self._get_test_jwt_token(user=jwt_user), + }) + + self.client.force_login(session_user) + response = self.client.get(reverse('authenticated-view')) + + mock_set_custom_attribute.assert_any_call('is_jwt_vs_session_user_check_enabled', True) + mock_set_custom_attribute.assert_any_call('jwt_auth_session_user_id', session_user_id) + mock_set_custom_attribute.assert_any_call('jwt_auth_and_session_user_mismatch', True) + mock_set_custom_attribute.assert_any_call('jwt_auth_result', 'success-cookie') + assert response.status_code == 200 + + @mock.patch('edx_rest_framework_extensions.auth.jwt.authentication.set_custom_attribute') + def test_authenticate_jwt_and_session_mismatch_bad_signature_cookie(self, mock_set_custom_attribute): + """ Tests monitoring for JWT cookie with a bad signature when there is a session user mismatch """ + session_user_id = 111 + session_user = factories.UserFactory(id=session_user_id) + jwt_user_id = 222 + jwt_user = factories.UserFactory(id=jwt_user_id) + self.client.cookies = SimpleCookie({ + jwt_cookie_name(): self._get_test_jwt_token(user=jwt_user, is_valid_signature=False), + }) + + enable_forgiving_jwt_cookies = get_setting(ENABLE_FORGIVING_JWT_COOKIES) + with override_settings( + EDX_DRF_EXTENSIONS={ + ENABLE_FORGIVING_JWT_COOKIES: enable_forgiving_jwt_cookies, + ENABLE_JWT_VS_SESSION_USER_CHECK: True, + }, + MIDDLEWARE=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + ), + ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_authentication', + ): + self.client.force_login(session_user) + response = self.client.get(reverse('authenticated-view')) + + mock_set_custom_attribute.assert_any_call('is_jwt_vs_session_user_check_enabled', True) + mock_set_custom_attribute.assert_any_call('jwt_auth_session_user_id', session_user_id) + mock_set_custom_attribute.assert_any_call('jwt_auth_and_session_user_mismatch', True) + mock_set_custom_attribute.assert_any_call('failed_jwt_cookie_user_id', jwt_user_id) + if enable_forgiving_jwt_cookies: + mock_set_custom_attribute.assert_any_call('jwt_auth_result', 'user-mismatch-failure') + else: + mock_set_custom_attribute.assert_any_call('jwt_auth_result', 'failed-cookie') + assert response.status_code == 401 + + @mock.patch('edx_rest_framework_extensions.auth.jwt.authentication.set_custom_attribute') + def test_authenticate_jwt_and_session_mismatch_invalid_cookie(self, mock_set_custom_attribute): + """ Tests monitoring for invalid JWT cookie when there is a session user mismatch """ + session_user_id = 111 + session_user = factories.UserFactory(id=session_user_id) + self.client.cookies = SimpleCookie({ + jwt_cookie_name(): 'invalid-cookie', + }) + + enable_forgiving_jwt_cookies = get_setting(ENABLE_FORGIVING_JWT_COOKIES) + with override_settings( + EDX_DRF_EXTENSIONS={ + ENABLE_FORGIVING_JWT_COOKIES: enable_forgiving_jwt_cookies, + ENABLE_JWT_VS_SESSION_USER_CHECK: True, + }, + MIDDLEWARE=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + ), + ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_authentication', + ): + self.client.force_login(session_user) + response = self.client.get(reverse('authenticated-view')) + + mock_set_custom_attribute.assert_any_call('is_jwt_vs_session_user_check_enabled', True) + mock_set_custom_attribute.assert_any_call('jwt_auth_session_user_id', session_user_id) + mock_set_custom_attribute.assert_any_call('jwt_auth_and_session_user_mismatch', True) + mock_set_custom_attribute.assert_any_call('failed_jwt_cookie_user_id', 'decode-error') + if enable_forgiving_jwt_cookies: + mock_set_custom_attribute.assert_any_call('jwt_auth_result', 'user-mismatch-failure') + else: + mock_set_custom_attribute.assert_any_call('jwt_auth_result', 'failed-cookie') + assert response.status_code == 401 + + @override_settings( + EDX_DRF_EXTENSIONS={ENABLE_JWT_VS_SESSION_USER_CHECK: True}, + MIDDLEWARE=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + ), + ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_authentication', + ) + @mock.patch('edx_rest_framework_extensions.auth.jwt.authentication.set_custom_attribute') + def test_authenticate_jwt_and_session_match(self, mock_set_custom_attribute): + """ Tests monitoring for JWT cookie when session user matches """ + test_user = factories.UserFactory() + self.client.cookies = SimpleCookie({ + jwt_cookie_name(): self._get_test_jwt_token(user=test_user), + }) + + self.client.force_login(test_user) + response = self.client.get(reverse('authenticated-view')) + + mock_set_custom_attribute.assert_any_call('is_jwt_vs_session_user_check_enabled', True) + set_custom_attribute_keys = [call.args[0] for call in mock_set_custom_attribute.call_args_list] + assert 'is_jwt_vs_session_user_check_enabled' in set_custom_attribute_keys + assert 'jwt_auth_session_user_id' not in set_custom_attribute_keys + assert 'jwt_auth_and_session_user_mismatch' not in set_custom_attribute_keys + assert response.status_code == 200 + + @override_settings( + EDX_DRF_EXTENSIONS={ENABLE_JWT_VS_SESSION_USER_CHECK: True}, + MIDDLEWARE=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + ), + ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_authentication', + ) + @mock.patch('edx_rest_framework_extensions.auth.jwt.authentication.set_custom_attribute') + def test_authenticate_jwt_and_no_session(self, mock_set_custom_attribute): + """ Tests monitoring for JWT cookie when there is no session """ + test_user = factories.UserFactory() + self.client.cookies = SimpleCookie({ + jwt_cookie_name(): self._get_test_jwt_token(user=test_user), + }) + + # unlike other tests, there is no force_login call to start the session + response = self.client.get(reverse('authenticated-view')) + + mock_set_custom_attribute.assert_any_call('is_jwt_vs_session_user_check_enabled', True) + set_custom_attribute_keys = [call.args[0] for call in mock_set_custom_attribute.call_args_list] + assert 'is_jwt_vs_session_user_check_enabled' in set_custom_attribute_keys + assert 'jwt_auth_session_user_id' not in set_custom_attribute_keys + assert 'jwt_auth_and_session_user_mismatch' not in set_custom_attribute_keys + assert response.status_code == 200 + + @override_settings( + EDX_DRF_EXTENSIONS={ENABLE_JWT_VS_SESSION_USER_CHECK: False}, + MIDDLEWARE=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + ), + ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_authentication', + ) + @mock.patch('edx_rest_framework_extensions.auth.jwt.authentication.set_custom_attribute') + def test_authenticate_jwt_and_session_mismatch_disabled(self, mock_set_custom_attribute): + """ Tests monitoring disabled for JWT cookie and session user mismatch """ + session_user = factories.UserFactory(id=111) + jwt_user = factories.UserFactory(id=222) + self.client.cookies = SimpleCookie({ + jwt_cookie_name(): self._get_test_jwt_token(user=jwt_user), + }) + + self.client.force_login(session_user) + response = self.client.get(reverse('authenticated-view')) + + mock_set_custom_attribute.assert_any_call('is_jwt_vs_session_user_check_enabled', False) + set_custom_attribute_keys = [call.args[0] for call in mock_set_custom_attribute.call_args_list] + assert 'is_jwt_vs_session_user_check_enabled' in set_custom_attribute_keys + assert 'jwt_auth_session_user_id' not in set_custom_attribute_keys + assert 'jwt_auth_and_session_user_mismatch' not in set_custom_attribute_keys + assert response.status_code == 200 + + def _get_test_jwt_token(self, user=None, is_valid_signature=True): """ Returns a user and jwt token """ - user = factories.UserFactory() - payload = generate_latest_version_payload(user) - jwt_token = generate_jwt_token(payload) + test_user = factories.UserFactory() if user is None else user + payload = generate_latest_version_payload(test_user) + if is_valid_signature: + jwt_token = generate_jwt_token(payload) + else: + jwt_token = generate_jwt_token(payload, signing_key='invalid-key') return jwt_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 84535d02..0ac9b1d6 100644 --- a/edx_rest_framework_extensions/auth/jwt/tests/test_decoder.py +++ b/edx_rest_framework_extensions/auth/jwt/tests/test_decoder.py @@ -13,6 +13,7 @@ decode_jwt_scopes, get_asymmetric_only_jwt_decode_handler, jwt_decode_handler, + unsafe_jwt_decode_handler, ) from edx_rest_framework_extensions.auth.jwt.tests.utils import ( generate_asymmetric_jwt_token, @@ -234,6 +235,20 @@ def test_keyset_size_and_other_monitoring(self, mock_set_custom_attribute): mock.call('jwt_auth_issuer_verification', 'matches-first-issuer'), ] + def test_unsafe_success_with_invalid_token(self): + """ + Verifies unsafe decode is successful, even with invalid claims and signature + """ + self.payload['iss'] = 'invalid-iss' + self.payload['exp'] = 'invalid-exp' + self.payload['aud'] = 'invalid-aud' + invalid_signing_key = 'invalid-secret-key' + + # Generate a token using the invalid signing key + token = generate_jwt_token(self.payload, invalid_signing_key) + decoded_token = unsafe_jwt_decode_handler(token) + assert decoded_token['username'] is not None + def _jwt_decode_handler_with_defaults(token): # pylint: disable=unused-argument """ diff --git a/edx_rest_framework_extensions/auth/jwt/tests/utils.py b/edx_rest_framework_extensions/auth/jwt/tests/utils.py index 81eeba5e..8cd565db 100644 --- a/edx_rest_framework_extensions/auth/jwt/tests/utils.py +++ b/edx_rest_framework_extensions/auth/jwt/tests/utils.py @@ -70,6 +70,7 @@ def generate_unversioned_payload(user): 'iss': jwt_issuer_data['ISSUER'], 'aud': jwt_issuer_data['AUDIENCE'], 'username': user.username, + 'user_id': user.id, # this should be conditionally added based on the scope 'email': user.email, 'iat': now, 'exp': now + ttl, diff --git a/edx_rest_framework_extensions/config.py b/edx_rest_framework_extensions/config.py index bf5e94c5..89279912 100644 --- a/edx_rest_framework_extensions/config.py +++ b/edx_rest_framework_extensions/config.py @@ -22,3 +22,17 @@ # .. toggle_target_removal_date: 2023-10-01 # .. toggle_tickets: https://github.com/openedx/edx-drf-extensions/issues/371 ENABLE_FORGIVING_JWT_COOKIES = 'ENABLE_FORGIVING_JWT_COOKIES' + +# .. toggle_name: EDX_DRF_EXTENSIONS[ENABLE_JWT_VS_SESSION_USER_CHECK] +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: If True, checks for mismatches of JWT cookie user and session user. If forgiving +# JWT cookies is also enabled, mismatches will result in an error, rather than being forgiving. Also +# adds monitoring of session user vs JWT cookie user. +# .. toggle_warning: Since edx-platform has user caching, this toggle is a safety valve in case it +# starts causing memory issues, as has happened in the past. See ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE. +# .. toggle_use_cases: temporary +# .. toggle_creation_date: 2023-10-04 +# .. toggle_target_removal_date: 2023-12-01 +# .. toggle_tickets: https://github.com/openedx/edx-drf-extensions/issues/371 +ENABLE_JWT_VS_SESSION_USER_CHECK = 'ENABLE_JWT_VS_SESSION_USER_CHECK' diff --git a/edx_rest_framework_extensions/settings.py b/edx_rest_framework_extensions/settings.py index 57572afa..6103e37e 100644 --- a/edx_rest_framework_extensions/settings.py +++ b/edx_rest_framework_extensions/settings.py @@ -17,6 +17,7 @@ from edx_rest_framework_extensions.config import ( ENABLE_FORGIVING_JWT_COOKIES, + ENABLE_JWT_VS_SESSION_USER_CHECK, ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE, ) @@ -35,6 +36,7 @@ 'JWT_PAYLOAD_MERGEABLE_USER_ATTRIBUTES': (), ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE: False, ENABLE_FORGIVING_JWT_COOKIES: False, + ENABLE_JWT_VS_SESSION_USER_CHECK: False, } diff --git a/test_settings.py b/test_settings.py index d1ac9ddc..dc0f0674 100644 --- a/test_settings.py +++ b/test_settings.py @@ -13,6 +13,7 @@ 'csrf.apps.CsrfAppConfig', 'django.contrib.auth', 'django.contrib.contenttypes', + 'django.contrib.sessions', 'edx_rest_framework_extensions', 'rest_framework_jwt', 'waffle',