diff --git a/openedx/core/djangoapps/oauth_dispatch/jwt.py b/openedx/core/djangoapps/oauth_dispatch/jwt.py index 0af40b18ac..f0d4dafb8d 100644 --- a/openedx/core/djangoapps/oauth_dispatch/jwt.py +++ b/openedx/core/djangoapps/oauth_dispatch/jwt.py @@ -61,6 +61,16 @@ def create_jwt_from_token(token_dict, oauth_adapter, use_asymmetric_key=None): access_token = oauth_adapter.get_access_token(token_dict['access_token']) client = oauth_adapter.get_client_for_token(access_token) + # .. setting_name: JWT_ACCESS_TOKEN_EXPIRE_SECONDS + # .. setting_default: 60 * 60 + # .. setting_description: The number of seconds a JWT access token remains valid. We use this + # custom setting for JWT formatted access tokens, rather than the django-oauth-toolkit setting + # ACCESS_TOKEN_EXPIRE_SECONDS, because the JWT is non-revocable and we want it to be shorter + # lived than the legacy Bearer (opaque) access tokens, and thus to have a smaller default. + # .. setting_warning: For security purposes, 1 hour (the default) is the maximum recommended setting + # value. For tighter security, you can use a shorter amount of time. + token_dict['expires_in'] = getattr(settings, 'JWT_ACCESS_TOKEN_EXPIRE_SECONDS', 60 * 60) + # TODO (ARCH-204) put access_token as a JWT ID claim (jti) return _create_jwt( access_token.user, diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/mixins.py b/openedx/core/djangoapps/oauth_dispatch/tests/mixins.py index ae4a40bc0a..ac755e5d5c 100644 --- a/openedx/core/djangoapps/oauth_dispatch/tests/mixins.py +++ b/openedx/core/djangoapps/oauth_dispatch/tests/mixins.py @@ -16,7 +16,8 @@ class AccessTokenMixin: """ Mixin for tests dealing with OAuth 2 access tokens. """ def assert_valid_jwt_access_token(self, access_token, user, scopes=None, should_be_expired=False, filters=None, - should_be_asymmetric_key=False, should_be_restricted=None, aud=None, secret=None): + should_be_asymmetric_key=False, should_be_restricted=None, aud=None, secret=None, + expires_in=None): """ Verify the specified JWT access token is valid, and belongs to the specified user. Returns: @@ -96,6 +97,9 @@ class AccessTokenMixin: self.assertDictContainsSubset(expected, payload) + if expires_in: + assert payload['exp'] == payload['iat'] + expires_in + # Since we suppressed checking of expiry # in the claim in the above check, because we want # to fully examine the claims outside of the expiry, diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py b/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py index 251f3db6a6..87931608b6 100644 --- a/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py +++ b/openedx/core/djangoapps/oauth_dispatch/tests/test_jwt.py @@ -4,6 +4,7 @@ from unittest.mock import patch import ddt from django.test import TestCase +from django.test.utils import override_settings from django.utils.timezone import now from openedx.core.djangoapps.oauth_dispatch import jwt as jwt_api @@ -66,6 +67,23 @@ class TestCreateJWTs(AccessTokenMixin, TestCase): jwt_token = self._create_jwt_for_token(DOTAdapter(), use_asymmetric_key=True) 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) + expected_expires_in = 60 * 60 + self.assert_valid_jwt_access_token( + jwt_token, self.user, self.default_scopes, expires_in=expected_expires_in, + ) + + def test_create_jwt_for_token_overridden_expire_seconds(self): + oauth_adapter = DOTAdapter() + expected_expires_in = 60 + with override_settings(JWT_ACCESS_TOKEN_EXPIRE_SECONDS=expected_expires_in): + jwt_token = self._create_jwt_for_token(oauth_adapter, use_asymmetric_key=False) + self.assert_valid_jwt_access_token( + jwt_token, self.user, self.default_scopes, expires_in=expected_expires_in, + ) + @ddt.data((True, False)) def test_dot_create_jwt_for_token(self, client_restricted): jwt_token = self._create_jwt_for_token( diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/test_views.py b/openedx/core/djangoapps/oauth_dispatch/tests/test_views.py index a0c51d51b1..7cd714e441 100644 --- a/openedx/core/djangoapps/oauth_dispatch/tests/test_views.py +++ b/openedx/core/djangoapps/oauth_dispatch/tests/test_views.py @@ -179,13 +179,15 @@ class TestAccessTokenView(AccessTokenLoginMixin, mixins.AccessTokenMixin, _Dispa response = self._post_request(self.user, client, token_type=token_type, headers=headers or {}) assert response.status_code == 200 data = json.loads(response.content.decode('utf-8')) - assert 'expires_in' in data + expected_default_expires_in = 60 * 60 + assert data['expires_in'] == expected_default_expires_in assert data['token_type'] == 'JWT' self.assert_valid_jwt_access_token( data['access_token'], self.user, data['scope'].split(' '), should_be_restricted=False, + expires_in=expected_default_expires_in, ) @ddt.data('dot_app')