jpadilla / django-rest-framework-jwt

JSON Web Token Authentication support for Django REST Framework
http://jpadilla.github.io/django-rest-framework-jwt/
MIT License
3.19k stars 651 forks source link

expire tokens on changing password #105

Open aliva opened 9 years ago

aliva commented 9 years ago

after changing my password my old tokens are still working, I think changing password should expire all old tokens,

I didn't find anything about that on docs, so I think it an issue with drf-jwt

ticosax commented 9 years ago

We could hash the secret with values extracted from the user. think password for instance. Once you change one of the values, the signature will never be the same again. Thus invalidates existing token for the user.

We could expose a list of attributes or a callable on the configuration layer to allow fine control for end users.

serjikisa commented 9 years ago

@ticosax : Can you please describe the details? (What should I change in the source code that token could sense password change)

Alex3917 commented 9 years ago

@ticosax Any reason not to just append the last few characters of the user's hashed password as a field on the token? This seems worth doing since right now if a user's token gets leaked there isn't any way to revoke all of their previous tokens.

ticosax commented 9 years ago

@serj1975 did you check the proof of concept in #112 ?

@Alex3917 I don't really grok your suggestion. Can you provide an example ?

Alex3917 commented 9 years ago

@ticosax

When a user creates a password, it gets hashed into something like:

pbkdf2_sha256$20000$FhnlJ0rpdCli$SHBxdnk2xUMmJgok//3YaC89uVJG0eu4xz14pWAakKE=

So when when a user obtains a new JWT, you would add, say, the last 15 characters of that user's password to the token:

jwt.update({ "lucky_numbers": "eu4xz14pWAakKE=" })

And then every time you validate the JWT, you check to see if the 'lucky_numbers' field on the token matches the last 15 characters on the user's password. If not, you raise an exception, and the token is no longer valid.

This is seems like a pretty good solution because you already need to lookup a user object to both create and validate a JWT, so there wouldn't be any extra database lookups. You could also add a DateTime field on the user model called something like 'password_last_changed_at', and invalidate all tokens issued before that timestamp. However, this involves forcing the user to add an extra field to their user model, and only really helps in the one in a quadrillion case that the last 15 digits of the old password hash are the same as for the new password hash.

Again, the use case is that if a user's token gets leaked, there is currently no way to invalidate all of the tokens that were issued before the leak occurred. (Except for changing the secret key, which would invalidate the tokens for the entire userbase.) This would provide a mechanism for doing that by just resetting the user's password, which in that situation should be done anyway.

nym commented 9 years ago

I also would like the token to be invalidated when pw changes.

ticosax commented 9 years ago

@Alex3917 thank you for the detailed explanation.

Your suggestion is viable, it rely on private claims names https://tools.ietf.org/html/rfc7519#section-4.3 of the spec. I'd say it could be considered as an alternative implementation, If we are not afraid to expose part of the hashed password. Which is something I'm not qualify to assert it is a safe operation ! Your suggestion has something better than mine also, is that, it can be pluggable and doesn't seems to require to change the internal API of drf-jwt.

Alex3917 commented 9 years ago

@ticosax The one issue I can think of here is if the user isn't using the default django password storage, e.g. if they mapped the user's password field to a preexisting database field that stores the passwords in plaintext or using a simple md5 hash or whatever. So if implemented there should probably be an option to disable this functionality, as well as a couple sentences about potential security implications.

Potentially someone could also make a rainbow table of common passwords depending on the length of the salts being used, and then if the last X chars of the hashes matched then presumably the password would also match. So that would be the other potential issue, and again how serious a threat this is probably depends on what defaults django uses for salts and whether or not the user is using the django defaults.

pySilver commented 9 years ago

What about sha1(user.password + SECRET_KEY) ? So later on you are just validating signatures and if it does not match – reject a token.

aliva commented 9 years ago

here is my workaround, I used this solution in our project (Pseudocode here), it may be helpful for this discussion:

I added this model to my app:

#models.py
class Token(models.Model):
    user= models.ForeignKey(settings.AUTH_USER_MODEL)
    token = models.CharField(max_length=200)
    password = models.CharField(max_length=200)

then I created my encode handler to add token to my db:

from rest_framework_jwt.utils import jwt_encode_handler
def my_encode_handler(payload):
    token = jwt_encode_handler(payload)

    current_password = get_user_model().objects.get(id=payload["user_id"]).password
    token.objects.create(payload["user_id"], token, current_password)

    return token

and then here is my decode handler

import jwt
from rest_framework_jwt.utils import jwt_decode_handler
def my_decode_handler(token):
    payload = jwt_decode_handler(token)

    user_id = payload.get("user_id")
    user = get_user_model().objects.get(id=user_id)

    try:
        Token.object.get(
            user=user,
            token=token,
            password=user.password # so if user have changed password they wont match here
        )
        return  payload
    except Token.objects.DoesNotExist:
            msg = 'can not use token'
            raise jwt.ExpiredSignature(msg)

so when user changes her password, my_decode_handler will stop login for old tokens or when they logout i simply pop that token from our db,

pySilver commented 9 years ago

So you basically killed all benefits of jwt? I think much easier would be using own payload generator + custom verification method.

Dnia 02.10.2015 o godz. 15:35 Ali Vakilzade notifications@github.com napisał(a):

here is my workaround, I used this solution in our project (Pseudocode here), it may be helpful for this discussion:

I added this model to my app:

models.py

class Token(models.Model): user= models.ForeignKey(settings.AUTH_USER_MODEL) token = models.CharField(max_length=200) password = models.CharField(max_length=200) then I created my encode handler to add token to my db:

from rest_framework_jwt.utils import jwt_encode_handler def my_encode_handler(payload): token = jwt_encode_handler(payload)

current_password = get_user_model().objects.get(id=payload["user_id"]).password
token.objects.create(payload["user_id"], token, current_password)

return token

and then here is my decode handler

import jwt from rest_framework_jwt.utils import jwt_decode_handler def my_decode_handler(token): payload = jwt_decode_handler(token)

user_id = payload.get("user_id")
user = get_user_model().objects.get(id=user_id)

try:
    Token.object.get(
        user=user,
        token=token,
        password=user.password # so if user have changed password they wont match here
    )
    return  payload
except Token.objects.DoesNotExist:
        msg = 'can not use token'
        raise jwt.ExpiredSignature(msg)

so when user changes her password, my_decode_handler will stop login for old tokens or when they logout i simply pop that token from our db,

— Reply to this email directly or view it on GitHub.

sandipbgt commented 8 years ago

Anybody found solutions to this?

Alex3917 commented 8 years ago

@sandipbgt In light of issue 244, adding a password_last_change datefield to your user model is definitely a better solution than using a part of the password hash, since having that datefield can also be used to mitigate that attack.

Aameer commented 8 years ago

+1 , any solution for this guys?

abukmca commented 7 years ago

from rest_framework.authtoken.models import Token def update (request): ""change the password"" Token.objects.get(user_id=request.user.id).delete() Token.objects.create(user_id=request.user.id)

leqnam commented 4 years ago

My solution is calling the delete() token method after .save() the object:

user.set_password(...)
user.save()
token = user.get_auth_token()
token.delete()