Search code examples
django-rest-frameworkjwtdjango-rest-authdjango-rest-framework-jwt

How to logout user when he changes password from all browsers (Django-rest-auth, JWT)?


First of all, i am new with django-rest-framework so please excuse me if I'm wrong.

I'm working with django-rest-auth and django-restframework-jwt to authenticate users. I'm saving the jwt token in localStorage everytime the user logs in.

The problem That I'm facing now is that when I log in with same credentials in two browsers and then I change password in one of them, the other account still valid and user still can navigate and see all pages even though the password has changed.

I wanted to make his JWT token invalid when he changes password so that he will be automatically logged out. But I couldn't find a way to expire his token in official documentation of Django REST framework JWT

I tried to track the moment of changing password by generating manually a new JWT token for user, but this is not working (maybe because the existing token is still valid)

@receiver(signals.pre_save, sender=User)
def revoke_tokens(sender, instance, **kwargs):
    existing_user = User.objects.get(pk=instance.pk)

    if getattr(settings, 'REST_USE_JWT', False):
        if instance.password != existing_user.password:
            # If user has changed his password, generate manually a new token for him
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
            payload = jwt_payload_handler(instance)
            payload['orig_iat'] = timegm(datetime.utcnow().utctimetuple())
            instance.token = jwt_encode_handler(payload)

After reading some documenations and posts, it seems that this is not quite easy with only jwt since it's stateless, But could somebody point me the direction where to go?

Should I remove JWT authentication?

Is there a work around that can help me on this ?

Thanks a lot.


EDIT:

I found a comment in a similar post on SO by @Travis stating that

A common approach for invalidating tokens when a user changes their password is to sign the token with a hash of their password. Thus if the password changes, any previous tokens automatically fail to verify. You can extend this to logout by including a last-logout-time in the user's record and using a combination of the last-logout-time and password hash to sign the token. This requires a DB lookup each time you need to verify the token signature, but presumably you're looking up the user anyway

I'm trying to implement that ..I will update my post if it worked. Otherwise, I still open to suggestions.


Solution

  • After days of work, I ended up by overriding the JWT_PAYLOAD_HANDLER and adding the last digits of the user's hash of password in the payload of JWT token (since adding all the hash of password in the payload is not a good practice) and then creating a custom middleware that intercepts all requests.

    in every request I check from jwt token if the hash of the password matches the existing user's hash (if not that means that the user has changed his password)

    if they are different then I raise an error and logout the user with old hash of password.

    in config file :

        'JWT_PAYLOAD_HANDLER': 'your.path.jwt.jwt_payload_handler',
    

    and in the root stated in the config file :

     def jwt_payload_handler(user):
          username_field = get_username_field()
          username = get_username(user)
    
          payload = {
        'user_id': user.pk,
        'username': username,
        'pwd': user.password[-10:],
        'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA
        }
    if hasattr(user, 'email'):
        payload['email'] = user.email
    if isinstance(user.pk, uuid.UUID):
        payload['user_id'] = str(user.pk)
    
          payload[username_field] = username
          return payload
    

    and then this is the custom middleware :

    from django.http.response import HttpResponseForbidden
    from django.utils.deprecation import MiddlewareMixin
    from rest_framework_jwt.utils import jwt_decode_handler
    from config.settings.base import JWT_AUTH
    from trp.users.models import User
    class JWTAuthenticationMiddleware(MiddlewareMixin):
       def process_request(self, request):
          jwt_user_pwd = self.get_jwt_user_pwd(request)
          # check if last digits of password read from jwt token matches the hash of the current user in DB
          if jwt_user_pwd is not None:
            if jwt_user_pwd['pwd'] != jwt_user_pwd['user'].password[-10:]:
                return HttpResponseForbidden()
    
    @staticmethod
    def get_jwt_user_pwd(request):
        token = request.META.get('HTTP_AUTHORIZATION', None)
        # Remove the prefix from token name so that decoding the token gives us correct credentials
        token = str(token).replace(JWT_AUTH['JWT_AUTH_HEADER_PREFIX'] + ' ', '')
        if token:
            try:
                payload = jwt_decode_handler(token)
                authenticated_user = User.objects.get(id=payload['user_id'])
            except Exception as e:
                authenticated_user = None
                payload = {}
            if authenticated_user and payload:
                return {'user': authenticated_user, 'pwd': payload.get('pwd')}
        return None
    

    To logout the user I have read the status code of the request 'in this case 403' from front end : (I'm using Angular in my case) and then logout the user I hope it helps someone in the future .