refactor: extract shareable jwt methods (#30451)

Extract some jwt related methods to enable reuse across views.
This is in preparation for a change to AccessTokenExchangeView.

Co-authored-by: jawad-khan <jawadkhan444@gmail.com>
This commit is contained in:
Robert Raposa
2022-05-25 08:53:36 -04:00
committed by GitHub
parent eb45e53799
commit d321ed5ccd
2 changed files with 68 additions and 27 deletions

View File

@@ -61,27 +61,33 @@ 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,
scopes=token_dict['scope'].split(' '),
expires_in=token_dict['expires_in'],
expires_in=get_jwt_access_token_expire_seconds(),
use_asymmetric_key=use_asymmetric_key,
is_restricted=oauth_adapter.is_client_restricted(client),
filters=oauth_adapter.get_authorization_filters(client),
)
def get_jwt_access_token_expire_seconds():
"""
Returns the number of seconds before a JWT access token expires.
.. 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.
"""
return getattr(settings, 'JWT_ACCESS_TOKEN_EXPIRE_SECONDS', 60 * 60)
def _create_jwt(
user,
scopes=None,

View File

@@ -17,7 +17,7 @@ from ratelimit.decorators import ratelimit
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_from_token
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_from_token, get_jwt_access_token_expire_seconds
class _DispatchingView(View):
@@ -75,6 +75,39 @@ class _DispatchingView(View):
return request.POST.get('client_id')
def _get_token_type(request):
"""
Get the token_type for the request.
- Respects the HTTP_X_TOKEN_TYPE header if the token_type parameter is not supplied.
- Adds `oauth_token_type` custom attribute for monitoring.
"""
default_token_type = request.META.get('HTTP_X_TOKEN_TYPE', 'no_token_type_supplied')
token_type = request.POST.get('token_type', default_token_type).lower()
monitoring_utils.set_custom_attribute('oauth_token_type', token_type)
return token_type
def _get_jwt_dict_from_access_token_dict(token_dict, oauth_adapter):
"""
Returns a JWT token dict from the provided original (opaque) access token dict.
Creates the new JWT, and then overrides various values in a copy of the
token dict with the JWT specific values.
"""
jwt_dict = token_dict.copy()
# TODO: It would be safer if create_jwt_from_token returned this
# dict directly, so it would not be possible for the dict and JWT
# to get out of sync, but that is a larger refactor to think through.
jwt = create_jwt_from_token(jwt_dict, oauth_adapter)
jwt_dict.update({
'access_token': jwt,
'token_type': 'JWT',
'expires_in': get_jwt_access_token_expire_seconds(),
})
return jwt_dict
@method_decorator(
ratelimit(
key='openedx.core.djangoapps.util.ratelimit.real_ip', rate=settings.RATELIMIT_RATE,
@@ -89,26 +122,25 @@ class AccessTokenView(_DispatchingView):
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
token_type = request.POST.get('token_type',
request.META.get('HTTP_X_TOKEN_TYPE', 'no_token_type_supplied')).lower()
monitoring_utils.set_custom_attribute('oauth_token_type', token_type)
monitoring_utils.set_custom_attribute('oauth_grant_type', request.POST.get('grant_type', ''))
monitoring_utils.set_custom_attribute('oauth_grant_type', request.POST.get('grant_type', 'not-supplied'))
token_type = _get_token_type(request)
if response.status_code == 200 and token_type == 'jwt':
response.content = self._build_jwt_response_from_access_token_response(request, response)
response.content = self._get_jwt_content_from_access_token_content(request, response)
return response
def _build_jwt_response_from_access_token_response(self, request, response):
""" Builds the content of the response, including the JWT token. """
token_dict = json.loads(response.content.decode('utf-8'))
jwt = create_jwt_from_token(token_dict, self.get_adapter(request))
token_dict.update({
'access_token': jwt,
'token_type': 'JWT',
})
return json.dumps(token_dict)
def _get_jwt_content_from_access_token_content(self, request, response):
"""
Gets the JWT response content from the original (opaque) token response content.
Includes the JWT token and token type in the response.
"""
opaque_token_dict = json.loads(response.content.decode('utf-8'))
jwt_token_dict = _get_jwt_dict_from_access_token_dict(
opaque_token_dict, self.get_adapter(request)
)
return json.dumps(jwt_token_dict)
class AuthorizationView(_DispatchingView):
@@ -128,5 +160,8 @@ class AccessTokenExchangeView(_DispatchingView):
class RevokeTokenView(_DispatchingView):
"""
Dispatch to the RevokeTokenView of django-oauth-toolkit
Note: JWT access tokens are non-revocable, but you could still revoke
its associated refresh_token.
"""
dot_view = dot_views.RevokeTokenView