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.
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 .