feat: Waffle switch to disable JWT for mobile (#31096)
* feat: Waffle switch to disable JWT for mobile
This commit is contained in:
@@ -13,11 +13,13 @@ from Cryptodome.PublicKey import RSA
|
||||
from django.conf import settings
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_switch
|
||||
from jwkest import jwk
|
||||
from oauth2_provider import models as dot_models
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.djangoapps.third_party_auth.tests.utils import ThirdPartyOAuthTestMixin, ThirdPartyOAuthTestMixinGoogle
|
||||
from openedx.core.djangoapps.oauth_dispatch.toggles import DISABLE_JWT_FOR_MOBILE
|
||||
|
||||
from . import mixins
|
||||
|
||||
@@ -349,6 +351,55 @@ class TestAccessTokenView(AccessTokenLoginMixin, mixins.AccessTokenMixin, _Dispa
|
||||
"""
|
||||
self._test_jwt_access_token('dot_app', token_type='jwt', grant_type='password', asymmetric_jwt=True)
|
||||
|
||||
@override_waffle_switch(DISABLE_JWT_FOR_MOBILE, False)
|
||||
@patch('openedx.core.djangoapps.oauth_dispatch.views.is_request_from_mobile_app')
|
||||
def test_disable_jwt_waffle_switch_disabled_non_mobile(self, mock_is_request_from_mobile_app):
|
||||
"""
|
||||
DISABLE_JWT_FOR_MOBILE disabled
|
||||
Non-mobile request
|
||||
Test JWT token is returned
|
||||
"""
|
||||
mock_is_request_from_mobile_app.return_value = False
|
||||
self._test_jwt_access_token('dot_app', token_type='jwt', grant_type='password')
|
||||
|
||||
@override_waffle_switch(DISABLE_JWT_FOR_MOBILE, True)
|
||||
@patch('openedx.core.djangoapps.oauth_dispatch.views.is_request_from_mobile_app')
|
||||
def test_disable_jwt_waffle_switch_enabled_non_mobile(self, mock_is_request_from_mobile_app):
|
||||
"""
|
||||
DISABLE_JWT_FOR_MOBILE enabled
|
||||
Non-mobile request
|
||||
Test JWT token is returned
|
||||
"""
|
||||
mock_is_request_from_mobile_app.return_value = False
|
||||
self._test_jwt_access_token('dot_app', token_type='jwt', grant_type='password')
|
||||
|
||||
@override_waffle_switch(DISABLE_JWT_FOR_MOBILE, True)
|
||||
@patch('openedx.core.djangoapps.oauth_dispatch.views.is_request_from_mobile_app')
|
||||
def test_disable_jwt_waffle_switch_enabled_mobile(self, mock_is_request_from_mobile_app):
|
||||
"""
|
||||
DISABLE_JWT_FOR_MOBILE enabled
|
||||
Mobile request
|
||||
Test JWT token is not returned, and that Bearer token is returned instead
|
||||
"""
|
||||
mock_is_request_from_mobile_app.return_value = True
|
||||
response = self._post_request(self.user, self.dot_app, token_type='jwt', asymmetric_jwt=True)
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
access_token = data['access_token']
|
||||
assert response.status_code == 200
|
||||
assert data['token_type'] == "Bearer"
|
||||
assert dot_models.AccessToken.objects.filter(token=access_token).exists() is True
|
||||
|
||||
@override_waffle_switch(DISABLE_JWT_FOR_MOBILE, False)
|
||||
@patch('openedx.core.djangoapps.oauth_dispatch.views.is_request_from_mobile_app')
|
||||
def test_disable_jwt_waffle_switch_disabled_mobile(self, mock_is_request_from_mobile_app):
|
||||
"""
|
||||
DISABLE_JWT_FOR_MOBILE disabled
|
||||
Mobile request
|
||||
Test JWT token is returned
|
||||
"""
|
||||
mock_is_request_from_mobile_app.return_value = True
|
||||
self._test_jwt_access_token('dot_app', token_type='jwt', grant_type='password', asymmetric_jwt=True)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@httpretty.activate
|
||||
@@ -376,6 +427,29 @@ class TestAccessTokenExchangeView(ThirdPartyOAuthTestMixinGoogle, ThirdPartyOAut
|
||||
|
||||
return body
|
||||
|
||||
def _test_jwt_access_token(self, client_attr, token_type=None, headers=None, grant_type=None, asymmetric_jwt=False):
|
||||
"""
|
||||
Test response for JWT token.
|
||||
"""
|
||||
client = getattr(self, client_attr)
|
||||
self.oauth_client = client
|
||||
self._setup_provider_response(success=True)
|
||||
response = self._post_request(self.user, client, token_type=token_type,
|
||||
headers=headers or {}, asymmetric_jwt=asymmetric_jwt)
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
assert 'expires_in' in data
|
||||
assert data['expires_in'] > 0
|
||||
assert data['token_type'] == 'JWT'
|
||||
|
||||
self.assert_valid_jwt_access_token(
|
||||
data['access_token'],
|
||||
self.user,
|
||||
data['scope'].split(' '),
|
||||
grant_type=grant_type,
|
||||
should_be_asymmetric_key=asymmetric_jwt
|
||||
)
|
||||
|
||||
@ddt.data('dot_app')
|
||||
def test_access_token_exchange_calls_dispatched_view(self, client_attr):
|
||||
client = getattr(self, client_attr)
|
||||
@@ -435,6 +509,62 @@ class TestAccessTokenExchangeView(ThirdPartyOAuthTestMixinGoogle, ThirdPartyOAut
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
assert data['error'] == 'account_disabled'
|
||||
|
||||
@override_waffle_switch(DISABLE_JWT_FOR_MOBILE, False)
|
||||
@patch('openedx.core.djangoapps.oauth_dispatch.views.is_request_from_mobile_app')
|
||||
@ddt.data('dot_app')
|
||||
def test_disable_jwt_waffle_switch_disabled_non_mobile(self, client_attr, mock_is_request_from_mobile_app):
|
||||
"""
|
||||
DISABLE_JWT_FOR_MOBILE disabled
|
||||
Non-mobile request
|
||||
Test JWT token is returned
|
||||
"""
|
||||
mock_is_request_from_mobile_app.return_value = False
|
||||
self._test_jwt_access_token(client_attr, token_type='jwt', grant_type='password')
|
||||
|
||||
@override_waffle_switch(DISABLE_JWT_FOR_MOBILE, True)
|
||||
@patch('openedx.core.djangoapps.oauth_dispatch.views.is_request_from_mobile_app')
|
||||
@ddt.data('dot_app')
|
||||
def test_disable_jwt_waffle_switch_enabled_non_mobile(self, client_attr, mock_is_request_from_mobile_app):
|
||||
"""
|
||||
DISABLE_JWT_FOR_MOBILE enabled
|
||||
Non-mobile request
|
||||
Test JWT token is returned
|
||||
"""
|
||||
mock_is_request_from_mobile_app.return_value = False
|
||||
self._test_jwt_access_token(client_attr, token_type='jwt', grant_type='password')
|
||||
|
||||
@override_waffle_switch(DISABLE_JWT_FOR_MOBILE, True)
|
||||
@patch('openedx.core.djangoapps.oauth_dispatch.views.is_request_from_mobile_app')
|
||||
@ddt.data('dot_app')
|
||||
def test_disable_jwt_waffle_switch_enabled_mobile(self, client_attr, mock_is_request_from_mobile_app):
|
||||
"""
|
||||
DISABLE_JWT_FOR_MOBILE enabled
|
||||
Mobile request
|
||||
Test JWT token is not returned, and that Bearer token is returned instead
|
||||
"""
|
||||
mock_is_request_from_mobile_app.return_value = True
|
||||
client = getattr(self, client_attr)
|
||||
self.oauth_client = client
|
||||
self._setup_provider_response(success=True)
|
||||
response = self._post_request(self.user, client, token_type='jwt', asymmetric_jwt=True)
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
access_token = data['access_token']
|
||||
assert response.status_code == 200
|
||||
assert data['token_type'] == "Bearer"
|
||||
assert dot_models.AccessToken.objects.filter(token=access_token).exists() is True
|
||||
|
||||
@override_waffle_switch(DISABLE_JWT_FOR_MOBILE, False)
|
||||
@patch('openedx.core.djangoapps.oauth_dispatch.views.is_request_from_mobile_app')
|
||||
@ddt.data('dot_app')
|
||||
def test_disable_jwt_waffle_switch_disabled_mobile(self, client_attr, mock_is_request_from_mobile_app):
|
||||
"""
|
||||
DISABLE_JWT_FOR_MOBILE disabled
|
||||
Mobile request
|
||||
Test JWT token is returned
|
||||
"""
|
||||
mock_is_request_from_mobile_app.return_value = True
|
||||
self._test_jwt_access_token(client_attr, token_type='jwt', grant_type='password', asymmetric_jwt=True)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
@ddt.ddt
|
||||
|
||||
24
openedx/core/djangoapps/oauth_dispatch/toggles.py
Normal file
24
openedx/core/djangoapps/oauth_dispatch/toggles.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Toggles for Oauth Dispatch.
|
||||
"""
|
||||
|
||||
|
||||
from edx_toggles.toggles import WaffleSwitch
|
||||
|
||||
OAUTH_DISPATCH_WAFFLE_SWITCH_NAMESPACE = 'oauth_dispatch'
|
||||
|
||||
# .. toggle_name: DISABLE_JWT_FOR_MOBILE
|
||||
# .. toggle_implementation: WaffleSwitch
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Set toggle to True to ignore mobile requests
|
||||
# for JWTs, and instead provide the legacy opaque Bearer token. This
|
||||
# toggle can be used as a temporary kill switch as the mobile app
|
||||
# attempts to rollout the transition to JWT authentication for the
|
||||
# first time.
|
||||
# .. toggle_use_cases: temporary
|
||||
# .. toggle_creation_date: 2022-10-14
|
||||
# .. toggle_target_removal_date: 2022-12-30
|
||||
DISABLE_JWT_FOR_MOBILE = WaffleSwitch(
|
||||
f'{OAUTH_DISPATCH_WAFFLE_SWITCH_NAMESPACE}.disable_jwt_for_mobile',
|
||||
__name__
|
||||
)
|
||||
@@ -18,6 +18,8 @@ from openedx.core.djangoapps.auth_exchange import views as auth_exchange_views
|
||||
from openedx.core.djangoapps.oauth_dispatch import adapters
|
||||
from openedx.core.djangoapps.oauth_dispatch.dot_overrides import views as dot_overrides_views
|
||||
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_token_dict
|
||||
from openedx.core.djangoapps.oauth_dispatch.toggles import DISABLE_JWT_FOR_MOBILE
|
||||
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
|
||||
|
||||
|
||||
class _DispatchingView(View):
|
||||
@@ -104,8 +106,13 @@ class AccessTokenView(_DispatchingView):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
monitoring_utils.set_custom_attribute('oauth_grant_type', request.POST.get('grant_type', 'not-supplied'))
|
||||
token_type = _get_token_type(request)
|
||||
is_jwt_disabled = False
|
||||
|
||||
if response.status_code == 200 and token_type == 'jwt':
|
||||
# Temporarily add control to disable jwt on mobile if needed
|
||||
if is_request_from_mobile_app(request):
|
||||
is_jwt_disabled = DISABLE_JWT_FOR_MOBILE.is_enabled()
|
||||
|
||||
if response.status_code == 200 and token_type == 'jwt' and not is_jwt_disabled:
|
||||
response.content = self._get_jwt_content_from_access_token_content(request, response)
|
||||
|
||||
return response
|
||||
@@ -139,8 +146,13 @@ class AccessTokenExchangeView(_DispatchingView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
token_type = _get_token_type(request)
|
||||
is_jwt_disabled = False
|
||||
|
||||
if response.status_code == 200 and token_type == 'jwt':
|
||||
# Temporarily add control to disable jwt on mobile if needed
|
||||
if is_request_from_mobile_app(request):
|
||||
is_jwt_disabled = DISABLE_JWT_FOR_MOBILE.is_enabled()
|
||||
|
||||
if response.status_code == 200 and token_type == 'jwt' and not is_jwt_disabled:
|
||||
response.data = self._get_jwt_data_from_access_token_data(request, response)
|
||||
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user