The recommended way to handle this is to separate token freshness from token validation. Most views require a valid token, secure views require a fresh token, one that is not only valid but also has been issued at login and has not been refreshed since.
You can do this by setting a flag on the token to mark it as 'fresh' on login, but unset the flag when refreshing. The flow then becomes:
- Client accesses the site without a token -> Deny access
- Client authenticates with obtain-token endpoint -> Issue token with
fresh=True
- Client (or server) refreshes valid token -> Issue token with
fresh=False
- Client accesses non-secure endpoint with valid token -> Accept token
- Client accesses secure endpoint -> Accept token only if
fresh=True is set on token.
The only way to obtain a fresh token is to log in again, no refresh allowed.
So you need to be able to:
- distinguish between obtaining a new token and refreshing when generating the JWT payload.
- inspect the
fresh key in the current JWT token in order to create a custom permission.
The first requirement means you'll have to do some view subclassing, as the jwt_payload_handler callback is not given any information to determine what called it.
The easiest way to handle the first requirement is to just subclass the serializers used to produce a fresh or refreshed token, decode the token they produce, inject the applicable fresh key value, then re-encode. Then use the subclassed serializers to create a new set of API views:
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.serializers import JSONWebTokenSerializer, RefreshJSONWebTokenSerializer
from rest_framework_jwt.views import JSONWebTokenAPIView
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
class FreshJSONWebTokenSerializer(JSONWebTokenSerializer):
"""Add a 'fresh=True' flag to the JWT token when issuing"""
def validate(self, *args, **kwargs):
result = super().validate(*args, **kwargs)
payload = jwt_decode_handler(result['token'])
return {
**result,
'token': jwt_encode_handler({**payload, fresh=True})
}
class NonFreshRefreshJSONWebTokenSerializer(RefreshJSONWebTokenSerializer):
"""Set the 'fresh' flag False on refresh"""
def validate(self, *args, **kwargs):
result = super().validate(*args, **kwargs)
payload = jwt_decode_handler(result['token'])
return {
**result,
'token': jwt_encode_handler({**payload, fresh=False})
}
class ObtainFreshJSONWebToken(JSONWebTokenAPIView):
serializer_class = JSONWebTokenSerializer
class NonFreshRefreshJSONWebToken(JSONWebTokenAPIView):
serializer_class = NonFreshRefreshJSONWebTokenSerializer
obtain_jwt_token = ObtainFreshJSONWebToken.as_view()
refresh_jwt_token = NonFreshRefreshJSONWebToken.as_view()
Then register these two views as API endpoints instead of those provided by the Django REST Framework JWT project, for the obtain and refresh paths.
Next up is the permission; because the JSONWebTokenAuthentication class returns the decoded payload when authenticating the request.auth attribute is set to the payload dictionary, letting us inspect it directly in a custom permission:
class HashFreshTokenPermission(permissions.BasePermission):
message = 'This endpoint requires a fresh token, please obtain a new token.'
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.auth and
isinstance(request.auth, dict) and
request.auth.get('fresh', False)
)
Register this permission with the REST framework:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
'yourmodule.HashFreshTokenPermission',
),
# ...
}
and use this in views that are security sensitive:
class SampleSecureView(APIView):
permission_classes = (
permissions.IsAuthenticated,
HashFreshTokenPermission,
)
# ...