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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user