1

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.

pietà
  • 760
  • 1
  • 11
  • 37

2 Answers2

1

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 .

pietà
  • 760
  • 1
  • 11
  • 37
0

Well,

It is all about token expiry time - If you keep this short (like 10-15 minutes) - you can no bother with invalidating it when a password or some permissions will change. Token will be invalidated always after some short period of time and a new one will be issued.

If you are using JWT as long living token (which is not good practice) - you will have problems.

Because actions like changing a password and invalidating other tokens (different session) (or force recreate) needs to be stored somewhere else (like some NoSQL store) - and checked for each session that some special action is required - and then you are losing the stateless advantage of JWT.

opalczynski
  • 1,599
  • 12
  • 14
  • Thanks for your reply, what should I use instead? should I remove authenticating with `JWT` ? in my case I have front and backend seperated : front with `angular` and back with `python/django` Thanks – pietà Apr 15 '19 at 08:32
  • Well, implement renew short token cycle on the frontend and backend :) Otherwise - allow your session to store some state - or store the JWT tokens somewhere and blacklist them when password change occurs. – opalczynski Apr 17 '19 at 16:54