From ef59da67750b6bd5b8c2275720faa4fc8ce41121 Mon Sep 17 00:00:00 2001 From: Ben Lu Date: Wed, 20 Mar 2024 14:20:57 -0500 Subject: [PATCH 1/2] feat: add JWT KID Added JWT KID when encoding and signing JWT with asymmetrical key --- openedx/core/djangoapps/oauth_dispatch/jwt.py | 16 +++++++++++ .../oauth_dispatch/tests/test_jwt.py | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/openedx/core/djangoapps/oauth_dispatch/jwt.py b/openedx/core/djangoapps/oauth_dispatch/jwt.py index cf13a2259b..de493715a7 100644 --- a/openedx/core/djangoapps/oauth_dispatch/jwt.py +++ b/openedx/core/djangoapps/oauth_dispatch/jwt.py @@ -276,6 +276,19 @@ def _attach_profile_claim(payload, user): 'superuser': user.is_superuser, }) +# .. toggle_name: JWT_AUTH_ADD_KID_HEADER +# .. toggle_implementation: SettingToggle +# .. toggle_default: False +# .. toggle_description: When True, add KID header to JWT using asymmetrical key. +# .. toggle_use_cases: temporary +# .. toggle_creation_date: 2024-03-20 +# .. toggle_target_removal_date: 2024-04-20 +# .. toggle_tickets: +# https://2u-internal.atlassian.net/browse/AUTH-195?atlOrigin=eyJpIjoiODMzODBiODMwMjU5NGRiZTkyOTIzYThhZjZiNWE0MzMiLCJwIjoiaiJ9 +JWT_AUTH_ADD_KID_HEADER = SettingToggle( + 'JWT_AUTH_ADD_KID_HEADER', default=False, module_name=__name__ +) + def _encode_and_sign(payload, use_asymmetric_key, secret): """Encode and sign the provided payload.""" @@ -289,6 +302,9 @@ def _encode_and_sign(payload, use_asymmetric_key, secret): algorithm = settings.JWT_AUTH['JWT_ALGORITHM'] jwk = PyJWK(key, algorithm) + if JWT_AUTH_ADD_KID_HEADER.is_enabled() and jwk.key_id: + return jwt.encode(payload, jwk.key, algorithm=algorithm, headers={'kid': jwk.key_id}) + return jwt.encode(payload, jwk.key, algorithm=algorithm) diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py b/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py index 7851158614..77d4d43b32 100644 --- a/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py +++ b/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import patch import ddt +import jwt from django.test import TestCase from django.test.utils import override_settings from django.utils.timezone import now @@ -79,6 +80,33 @@ class TestCreateJWTs(AccessTokenMixin, TestCase): jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=False) self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=True) + def test_kid_not_in_jwt_header_with_symmetric_key_and_kid_disabled(self): + jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=False) + header = jwt.get_unverified_header(jwt_token) + assert 'kid' not in header + self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=False) + + def test_kid_not_in_jwt_header_with_asymmetric_key_and_kid_disabled(self): + jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=True) + header = jwt.get_unverified_header(jwt_token) + assert 'kid' not in header + self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=True) + + @override_settings(JWT_AUTH_ADD_KID_HEADER=True) + def test_kid_not_in_jwt_header_with_symmetric_key_and_kid_enabled(self): + jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=False) + header = jwt.get_unverified_header(jwt_token) + assert 'kid' not in header + self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=False) + + @override_settings(JWT_AUTH_ADD_KID_HEADER=True) + def test_kid_in_jwt_header_with_asymmetric_key_and_kid_enabled(self): + jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=True) + header = jwt.get_unverified_header(jwt_token) + assert 'kid' in header + assert header['kid'] == 'BTZ9HA6K' + self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=True) + def test_create_jwt_for_token_default_expire_seconds(self): oauth_adapter = DOTAdapter() jwt_token = self._create_jwt_for_token(oauth_adapter, use_asymmetric_key=False) From 6112f85c0b61d23016076f2a66353a64495d4ea8 Mon Sep 17 00:00:00 2001 From: Ben Lu Date: Wed, 3 Apr 2024 13:22:51 -0500 Subject: [PATCH 2/2] fixup! feat: add JWT KID --- openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py b/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py index 77d4d43b32..da95fd072d 100644 --- a/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py +++ b/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py @@ -3,7 +3,6 @@ from datetime import timedelta from unittest.mock import patch import ddt -import jwt from django.test import TestCase from django.test.utils import override_settings from django.utils.timezone import now @@ -82,27 +81,27 @@ class TestCreateJWTs(AccessTokenMixin, TestCase): def test_kid_not_in_jwt_header_with_symmetric_key_and_kid_disabled(self): jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=False) - header = jwt.get_unverified_header(jwt_token) + header = jwt_api.jwt.get_unverified_header(jwt_token) assert 'kid' not in header self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=False) def test_kid_not_in_jwt_header_with_asymmetric_key_and_kid_disabled(self): jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=True) - header = jwt.get_unverified_header(jwt_token) + header = jwt_api.jwt.get_unverified_header(jwt_token) assert 'kid' not in header self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=True) @override_settings(JWT_AUTH_ADD_KID_HEADER=True) def test_kid_not_in_jwt_header_with_symmetric_key_and_kid_enabled(self): jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=False) - header = jwt.get_unverified_header(jwt_token) + header = jwt_api.jwt.get_unverified_header(jwt_token) assert 'kid' not in header self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=False) @override_settings(JWT_AUTH_ADD_KID_HEADER=True) def test_kid_in_jwt_header_with_asymmetric_key_and_kid_enabled(self): jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=True) - header = jwt.get_unverified_header(jwt_token) + header = jwt_api.jwt.get_unverified_header(jwt_token) assert 'kid' in header assert header['kid'] == 'BTZ9HA6K' self._assert_jwt_is_valid(jwt_token, should_be_asymmetric_key=True)