Search code examples
djangodjango-rest-frameworkjwtdjango-authenticationdjango-contrib

How do I use the Django rest framework to prolong a JWT session token?


I'm using Django 3.2 with the django.auth.contrib app and djangorestframework-jwt==1.11.0. How do I prolong/reissue a new session token upon receiving a request for an authenticated resource and validating the user can access that resource? I use the following serializer and view to login the user and issue the initial token

class UserLoginSerializer(serializers.Serializer):

    username = serializers.CharField(max_length=255)
    password = serializers.CharField(max_length=128, write_only=True)
    token = serializers.CharField(max_length=255, read_only=True)

    def validate(self, data):
        username = data.get("username", None)
        password = data.get("password", None)
        user = authenticate(username=username, password=password)
        if user is None:
            raise serializers.ValidationError(
                'A user with this email and password is not found.'
            )
        try:
            payload = JWT_PAYLOAD_HANDLER(user)
            jwt_token = JWT_ENCODE_HANDLER(payload)
            update_last_login(None, user)
        except User.DoesNotExist:
            raise serializers.ValidationError(
                'User with given email and password does not exists'
            )
        return {
            'username':user.username,
            'token': jwt_token
        }

class UserLoginView(RetrieveAPIView):

    permission_classes = (AllowAny,)
    serializer_class = UserLoginSerializer

    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        response = {
            'success' : 'True',
            'status code' : status.HTTP_200_OK,
            'message': 'User logged in successfully',
            'token' : serializer.data['token'],
            }
        status_code = status.HTTP_200_OK

        return Response(response, status=status_code)

I have this in my settings file to keep the session to 1 hour initially

JWT_AUTH = {
    # how long the original token is valid for
    'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=1),

}

The client submits the session token in the "Authorization" header and it is validated (for example) using the below view

class UserProfileView(RetrieveAPIView):

    permission_classes = (IsAuthenticated,)
    authentication_class = JSONWebTokenAuthentication

    def get(self, request):
        try:
            token = get_authorization_header(request).decode('utf-8')
            if token is None or token == "null" or token.strip() == "":
                raise exceptions.AuthenticationFailed('Authorization Header or Token is missing on Request Headers')
            decoded = jwt.decode(token, settings.SECRET_KEY)
            username = decoded['username']
            
            status_code = status.HTTP_200_OK
            response = {
                'success': 'true',
                'status code': status_code,
                'message': 'User profile fetched successfully',
                'data': {
                        #...
                    }
                }

        except Exception as e:
            status_code = status.HTTP_400_BAD_REQUEST
            response = {
                'success': 'false',
                'status code': status.HTTP_400_BAD_REQUEST,
                'message': 'User does not exists',
                'error': str(e)
                }
        return Response(response, status=status_code)

What I would like to do in my response is send a new session token down to the user that is good for another hour but I'm unclear what call I need to make to generate such a token and/or edit/invalidate the existing one.


Solution

  • It is not possible to change a JWT after it is issued, so you can not extend its lifetime, but you can do something like this:

    for every request client makes:
        if JWT is expiring:
           generate a new JWT and add it to the response
    

    And the client will use this newly issued token.
    for this, you can add a django middleware: **EDITED

    class ExtendJWTToResponse:
        def __init__(self, get_response):
            self.get_response = get_response
            # One-time configuration and initialization.
    
        def __call__(self, request):
            # Code to be executed for each request before
            # the view (and later middleware) are called.
            
            jwt_token = get_authorization_header(request).decode('utf-8')
            new_jwt_token = None
            try:
                payload = jwt.decode(jwt_token, settings.SECRET_KEY)
                new_jwt_token = JWT_ENCODE_HANDLER(payload)
            except  PyJWTError:
                pass
                
            response = self.get_response(request)
    
            # Code to be executed for each request/response after
            # the view is called.
            if new_jwt_token:
                response['Refresh-Token'] = new_jwt_token
    
            return response
    

    And the client must check on 'Refresh-Token' header on response and if there is any it should replace the token and use the newly issued token with the extended lifetime.
    note: it is better to throttle issuing new tokens, for example, every time the request token is going to expire in the next 20 minutes...