Skip to content

Commit

Permalink
feat: add check for enforcing jwt and lms user email match (#419)
Browse files Browse the repository at this point in the history
* feat: add check for enforcing jwt and lms user email match
---------

Co-authored-by: Robert Raposa <[email protected]>
  • Loading branch information
syedsajjadkazmii and robrap authored Jan 29, 2024
1 parent de055fb commit 97bc367
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 4 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ Change Log
Unreleased
----------

[10.1.0] - 2024-01-26
---------------------

* Added permanent toggle EDX_DRF_EXTENSIONS[ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH]:

* This toggle should only get enabled in the LMS, and should remain disabled in all other services.
* If enabled, makes sure that the user email in JWT cookies and LMS user email matches
* If email matches, it allows authentication otherwise raise JwtUserEmailMismatchError error.

[10.0.0] - 2023-11-30
---------------------

Expand Down
2 changes: 1 addition & 1 deletion edx_rest_framework_extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
""" edx Django REST Framework extensions. """

__version__ = '10.0.0' # pragma: no cover
__version__ = '10.1.0' # pragma: no cover
34 changes: 33 additions & 1 deletion edx_rest_framework_extensions/auth/jwt/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
configured_jwt_decode_handler,
unsafe_jwt_decode_handler,
)
from edx_rest_framework_extensions.config import ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE
from edx_rest_framework_extensions.config import (
ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH,
ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE,
)
from edx_rest_framework_extensions.settings import get_setting


Expand All @@ -31,6 +34,10 @@ class JwtSessionUserMismatchError(JwtAuthenticationError):
pass


class JwtUserEmailMismatchError(JwtAuthenticationError):
pass


class CSRFCheck(CsrfViewMiddleware):
def _reject(self, request, reason):
# Return the failure reason instead of an HttpResponse
Expand Down Expand Up @@ -103,6 +110,14 @@ def authenticate(self, request):
set_custom_attribute('jwt_auth_result', 'n/a')
return user_and_auth

if get_setting(ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH):
is_email_mismatch = self._is_jwt_and_lms_user_email_mismatch(request, user_and_auth[0])
if is_email_mismatch:
raise JwtUserEmailMismatchError(
'Failing JWT authentication due to jwt user email mismatch '
'with lms user email.'
)

# Not using JWT cookie, CSRF validation not required
if not is_authenticating_with_jwt_cookie:
set_custom_attribute('jwt_auth_result', 'success-auth-header')
Expand Down Expand Up @@ -313,6 +328,23 @@ def _is_jwt_cookie_and_session_user_mismatch(self, request):

return True

def _is_jwt_and_lms_user_email_mismatch(self, request, user):
"""
Returns True if user email in JWT and email of user do not match, False otherwise.
Arguments:
request: The request.
user: user from user_and_auth
"""
lms_user_email = getattr(user, 'email', None)

# This function will check for token in the authorization header and return it
# otherwise it will return token from JWT cookies.
token = JSONWebTokenAuthentication.get_token_from_request(request)
decoded_jwt = configured_jwt_decode_handler(token)
jwt_user_email = decoded_jwt.get('email', None)

return lms_user_email != jwt_user_email

def _get_unsafe_jwt_cookie_username_and_lms_user_id(self, request):
"""
Returns a tuple of the (username, lms user id) from the JWT cookie, or (None, None) if not found.
Expand Down
116 changes: 115 additions & 1 deletion edx_rest_framework_extensions/auth/jwt/tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
generate_jwt_token,
generate_latest_version_payload,
)
from edx_rest_framework_extensions.config import ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE
from edx_rest_framework_extensions.config import (
ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH,
ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE,
)
from edx_rest_framework_extensions.tests import factories


Expand Down Expand Up @@ -534,6 +537,117 @@ def test_authenticate_jwt_and_no_session_and_set_request_user(self, mock_set_cus
assert 'jwt_auth_mismatch_jwt_cookie_username' not in set_custom_attribute_keys
assert response.status_code == 200

@override_settings(
MIDDLEWARE=(
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'edx_rest_framework_extensions.auth.jwt.middleware.JwtAuthCookieMiddleware',
),
ROOT_URLCONF='edx_rest_framework_extensions.auth.jwt.tests.test_authentication',
)
def test_authenticate_user_lms_and_jwt_email_mismatch_toggle_disabled(self):
"""
Test success for JwtAuthentication when ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH is disabled.
"""
user = factories.UserFactory(email='[email protected]')
jwt_header_payload, jwt_signature = self._get_test_jwt_token_payload_and_signature(user=user)

# Cookie parts will be recombined by JwtAuthCookieMiddleware
self.client.cookies = SimpleCookie({
jwt_cookie_header_payload_name(): jwt_header_payload,
jwt_cookie_signature_name(): jwt_signature,
})

# simulating email change
user.email = '[email protected]'
user.save() # pylint: disable=no-member

self.client.force_login(user)

response = self.client.get(reverse('authenticated-view'))

assert response.status_code == 200

@override_settings(
EDX_DRF_EXTENSIONS={
ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH: True,
'JWT_PAYLOAD_USER_ATTRIBUTE_MAPPING': {},
'JWT_PAYLOAD_MERGEABLE_USER_ATTRIBUTES': []
},
MIDDLEWARE=(
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'edx_rest_framework_extensions.auth.jwt.middleware.JwtAuthCookieMiddleware',
),
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_user_lms_and_jwt_email_match_failure(self, mock_set_custom_attribute):
"""
Test failure for JwtAuthentication when ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH
is enabled and the lms and jwt user email do not match.
"""
user = factories.UserFactory(email='[email protected]')
jwt_header_payload, jwt_signature = self._get_test_jwt_token_payload_and_signature(user=user)

# Cookie parts will be recombined by JwtAuthCookieMiddleware
self.client.cookies = SimpleCookie({
jwt_cookie_header_payload_name(): jwt_header_payload,
jwt_cookie_signature_name(): jwt_signature,
})

# simulating email change
user.email = '[email protected]'
user.save() # pylint: disable=no-member

self.client.force_login(user)

response = self.client.get(reverse('authenticated-view'))

assert response.status_code == 401
mock_set_custom_attribute.assert_any_call(
'jwt_auth_failed',
"Exception:JwtUserEmailMismatchError('Failing JWT authentication due to jwt user email mismatch with lms "
"user email.')"
)

@override_settings(
EDX_DRF_EXTENSIONS={
ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH: True,
'JWT_PAYLOAD_USER_ATTRIBUTE_MAPPING': {},
'JWT_PAYLOAD_MERGEABLE_USER_ATTRIBUTES': []
},
MIDDLEWARE=(
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'edx_rest_framework_extensions.auth.jwt.middleware.JwtAuthCookieMiddleware',
),
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_user_lms_and_jwt_email_match_success(self, mock_set_custom_attribute):
"""
Test success for JwtAuthentication when ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH
is enabled and the lms and jwt user email match.
"""
user = factories.UserFactory(email='[email protected]')
jwt_header_payload, jwt_signature = self._get_test_jwt_token_payload_and_signature(user=user)

# Cookie parts will be recombined by JwtAuthCookieMiddleware
self.client.cookies = SimpleCookie({
jwt_cookie_header_payload_name(): jwt_header_payload,
jwt_cookie_signature_name(): jwt_signature,
})

# Not changing email

self.client.force_login(user)

response = self.client.get(reverse('authenticated-view'))

assert response.status_code == 200
mock_set_custom_attribute.assert_any_call('jwt_auth_result', 'success-cookie')

def _get_test_jwt_token(self, user=None, is_valid_signature=True, lms_user_id=None):
""" Returns a test jwt token for the provided user """
test_user = factories.UserFactory() if user is None else user
Expand Down
11 changes: 11 additions & 0 deletions edx_rest_framework_extensions/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@
# work in all services, or find a replacement. Consider making this a permanent toggle instead.
# .. toggle_tickets: ARCH-1210, ARCH-1199, ARCH-1197
ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE = 'ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE'

# .. toggle_name: EDX_DRF_EXTENSIONS[ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH]
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Toggle to add a check for matching user email in JWT and LMS user email
# for authentication in JwtAuthentication class. This toggle should only be enabled in the
# LMS as our identity service that is also creating the JWTs.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2023-12-20
# .. toggle_tickets: VAN-1694
ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH = 'ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH'
6 changes: 5 additions & 1 deletion edx_rest_framework_extensions/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
from django.conf import settings
from rest_framework_jwt.settings import api_settings

from edx_rest_framework_extensions.config import ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE
from edx_rest_framework_extensions.config import (
ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH,
ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE,
)


logger = logging.getLogger(__name__)


DEFAULT_SETTINGS = {
ENABLE_JWT_AND_LMS_USER_EMAIL_MATCH: False,
ENABLE_SET_REQUEST_USER_FOR_JWT_COOKIE: False,

'JWT_PAYLOAD_MERGEABLE_USER_ATTRIBUTES': (),
Expand Down

0 comments on commit 97bc367

Please sign in to comment.