Relogin after N minutes with django and JWT

Viewed 1010

Scenario: I want a user to re-login when passing to a security sensible area after N minutes, e.g. when user is about to pay an order, however he logged in 1 hour ago, I would like to be sure it's him. This by using rest_framework_jwt.

Long description:

I've been recently testing django for modern web development (so, backend with rest-api). However, I encountered a problem which I have not yet found a solution.

In rest_framework_jwt you set the authentication class as follows.

'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework_jwt.authentication.JSONWebTokenAuthentication',

This will do a great work for the general purpose. However, I want the user to reidentify (re-login) when entering an area with sensible information 10 minutes after login, e.g. sensible information can be a payment area. Thus, I would like to send a parameter to the Authentication class telling that the user is in a sensible area.

What I think as a possible solution but I don't know how to do it yet, is: The rest_framework_jwt creates the variable orig_iat when using the option JWT_ALLOW_REFRESH. I could send a flag to authentication class to tell that current view is a sensitive area or not, if so and the user logged in more than 10 minutes ago, I can send a message to say that the user needs to re-login to continue.

I don't mind forking the rest_framework_jwt project and adapting it for my purposes, however I would like to know how to send the parameter from the view to the authentication class (in this case: rest_framework_jwt.authentication.JSONWebTokenAuthentication).

Also, If there is already something done with rest_framework_jwt for this scenario, I would like to avoid re-inventing the wheel.

3 Answers

Well... So far, what I've done it's to create a decorator for a function view. The code for the decorator is as follows:

from functools import wraps
from rest_framework_jwt.settings import api_settings
from django.utils.translation import ugettext as _
from calendar import timegm
import datetime
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
def recently_authenticated():
    def decorator(func):
        @wraps(func)
        def inner(request, *args, **kwargs):
            jwt_payload = jwt_decode_handler(request._auth)
            rencent_auth_limit = api_settings.JWT_RECENT_AUTHENTICATION_DELTA
            if isinstance(rencent_auth_limit, datetime.timedelta):
                rencent_auth_limit = (rencent_auth_limit.days * 24 * 3600 +
                                 rencent_auth_limit.seconds) + jwt_payload["orig_iat"]
            timenow = timegm(datetime.datetime.utcnow().utctimetuple())
            if timenow>rencent_auth_limit:
                return Response({"detail":_("you have to reidentify to enter this area")},
                                status=401)
            return func(request, *args, **kwargs)
        return inner
    return decorator

The response format is given in the same format as rest_framework_jwt.authentication.JSONWebTokenAuthentication. The constant JWT_RECENT_AUTHENTICATION_DELTA is an ad-hoc parameter inserted in the settings.py of the rest_framework_jwt package (a fork).

Finally, in order to use it, one can add the decorator to any view. For example:

@api_view()
@recently_authenticated()
def index_view(request):
    data = User.objects.filter()
    return Response(UserSerializer(data, many=True).data)

And when the user has been authenticated a while ago, it will send the message {"detail":"you have to reidentify to enter this area"} with the code 401. This can be evaluated and parsed by the frontend and redirect the user to the login.

Note: The decorator only evaluates the time and the time passed. The verification telling whether or not it's a correct user and a correct token is still performed by rest_framework_jwt.authentication.JSONWebTokenAuthentication.

According to the manual: https://getblimp.github.io/django-rest-framework-jwt/#additional-settings

Disallowing refresh token should do the job. The thing is that you will get only 1 token and you won't be able to refresh it after 1 hour.

JWT_AUTH = {
    'JWT_ALLOW_REFRESH': False,
    'JWT_REFRESH_EXPIRATION_DELTA': timedelta(hours=1),
}

Other problems should be solved on the frontend side. You should check whether user is trying to get to "sensitive" view. If yes, then check if token is valid. If invalid - redirect to login page. If view is "insensitive" - your choice.

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:

  1. Client accesses the site without a token -> Deny access
  2. Client authenticates with obtain-token endpoint -> Issue token with fresh=True
  3. Client (or server) refreshes valid token -> Issue token with fresh=False
  4. Client accesses non-secure endpoint with valid token -> Accept token
  5. 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,
    )
    # ...
Related