feat: Waffle switch to disable JWT for mobile (#31096)

* feat: Waffle switch to disable JWT for mobile
This commit is contained in:
Moeez Zahid
2022-10-17 16:57:11 +05:00
committed by GitHub
parent b0774c6d97
commit 4271e24eb9
3 changed files with 168 additions and 2 deletions

View File

@@ -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

View 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__
)

View File

@@ -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