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

Tokens won't refresh after expiration date despite high refresh expiration delta #92

Open MasterKale opened 9 years ago

MasterKale commented 9 years ago

I've implemented rest_framework_jwt and have it working for authentication when accessing my API. However, I'm unable to refresh any tokens if it's after the token's exp value despite setting JWT_REFRESH_EXPIRATION_DELTA to 30 days.

For example, if I have the following settings...

JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=900),
    'JWT_ALLOW_REFRESH': True,
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=30),
}

...then shouldn't I be able to refresh the token within 30 days of the token's orig_iat date? Am I misreading the settings documentation for JWT_REFRESH_EXPIRATION_DELTA?

Out of curiosity I dug into PyJWT and found out that its decode() function only checks that the expiration date is later than utcnow()+leeway:

if 'exp' in payload and verify_expiration:
    utc_timestamp = timegm(datetime.utcnow().utctimetuple())

    if payload['exp'] < (utc_timestamp - leeway):
        raise ExpiredSignatureError('Signature has expired')

Going back into RefreshJSONWebTokenSerializer reveals that the token isn't getting past the initial payload check (which calls PyJWT's decode() function) to determine if the current time is within orig_iat + RefreshJSONWebTokenSerializer.

Is this just a huge misunderstanding on my part as to how JWTs work?

MasterKale commented 9 years ago

@erichonkanen On the client side of things, this is the response I get when I try to refresh an expired token:

HTTP/1.0 400 BAD REQUEST
Allow: POST, OPTIONS
Content-Type: application/json
Date: Wed, 01 Apr 2015 16:55:41 GMT
Server: WSGIServer/0.2 CPython/3.4.3
X-Frame-Options: SAMEORIGIN

{
    "non_field_errors": [
        "Signature has expired."
    ]
}

And here's console output from Django (I added some debug print()s to help troubleshoot):

System check identified no issues (0 silenced).
April 01, 2015 - 09:55:38
Django version 1.7.7, using settings 'tutorial.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
DEBUG - RefreshJSONWebTokenSerializer: checking payload
DEBUG - JWT: Expiration timestamp is earlier than 1427907341
DEBUG - ExpiredSignature Exception @ VerificationBaseSerializer

As for leeway I'm using the default value of 0 seconds. And I've confirmed multiple times that I can refresh valid tokens (including descendant tokens created when I refresh a token) up until they expire.

alarrosa14 commented 9 years ago

I have exactly the same doubt here. Shouldn't we be getting a refresh_token apart from id_token when we request for jwt with user+password?

vikitripathi commented 9 years ago

I too am having the same issue . Actually when i try to make a request to the API after setting the header with jwt token, i get

{"detail": "Authentication credentials were not provided."}

But when i try to make a curl request with that same token i get "toke is expired" So in curl i make another request to http://localhost:8005/api-token-auth/ with credentials , which gives me my new token and if i now send the curl request using it , it gets me the detail from API.

So my question is that do we need to do it the same way programmatically ? that is check if token is expired and renew it or there is some functionality like that and its some issue ?

sklirg commented 9 years ago

I'm also experiencing this issue.

Simple test:

Using these settings,

JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=1),  # Token expires * minutes after being issued
     'JWT_ALLOW_REFRESH': True,
     'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(minutes=5),  # Token can be refreshed up to * minutes after being issued
 }

and then refreshing token if timedelta passes 30 seconds, works. By works, I mean I can successfully refresh the token an get a new one in the response.

However, if I now choose to refresh the token after 65 seconds, I will get a 400 BAD REQUEST with the following;

{"non_field_errors":["Signature has expired."]}

I am refreshing the token after the expiration delta, but before the refresh expiration delta.


Docs:

JWT_EXPIRATION_DELTA This is an instance of Python's datetime.timedelta. This will be added to datetime.utcnow() to set the expiration time.

Default is datetime.timedelta(seconds=300)(5 minutes).

JWT_REFRESH_EXPIRATION_DELTA Limit on token refresh, is a datetime.timedelta instance. This is how much time after the original token that future tokens can be refreshed from.

Default is datetime.timedelta(days=7) (7 days).

My understanding of this tells me that I can create a token, and refresh it for up to a week after creation. However, if the token expires, that won't work, as I just demonstrated.

marcussaad commented 9 years ago

+1 on having the same issue.

gabn88 commented 9 years ago

+1 same issue

arpheno commented 9 years ago

I think this is working as intended :

Note that only non-expired tokens will work. 
sklirg commented 9 years ago

I disagree.

JWT_REFRESH_EXPIRATION_DELTA Limit on token refresh, is a datetime.timedelta instance. This is how much time after the original token that future tokens can be refreshed from.

ajostergaard commented 9 years ago

The docs are actually pretty clear: http://getblimp.github.io/django-rest-framework-jwt/#refresh-token

The refresh expiration delta allows you to continue refreshing tokens as long as you do so while you still have an unexpired token.

Works as intended.

gabn88 commented 9 years ago

It is nice that people are referring to the actual documents to decide that it works as intended, but let's stick to what one of the early proponents of JWT decided (https://auth0.com/docs/refresh-token). This means that while an REFRESH token is unexpired (!! independend of the original jwt), you can use the refresh token to refresh your jwt. This means that a quick fix is needed, in my humble opinion.

I'd like to hear further thoughts on this and reasons why to stick to the documentation.

Main reason for the behaviour such as described by https://auth0.com/docs/refresh-token is: You can have a really short duration for you JWT (let's say 15 minutes or less), so a really short amount of time for someone who hijacked a users token to use it, while still not requiring your users to login every 15 minutes. Only when the refresh token expires your users need to login again. The expiry for the refresh token can then be set to for example 1 year.

Although one can argue how much more safe it is (you should be using https anyway, and if someone breaks into the client app, why wouldn't they steal the refresh token as well?) at least this behaviour makes sense. Otherwise the whole refresh token part can just as well be left out in my opinion...

sklirg commented 9 years ago

:+1:

ajostergaard commented 9 years ago

@gabn88 I don't think you'll find refresh tokens (as a specific type of token) mentioned in the standard (https://tools.ietf.org/html/rfc7519) so don't believe DRF JWT should support them. Of course I'm no authority - this is just my tuppence. :)

arpheno commented 9 years ago

If they steal a token they have the username and password no?

On Wed, Oct 21, 2015, 07:54 AJ notifications@github.com wrote:

@gabn88 https://github.com/gabn88 I don't think you'll find refresh tokens (as a specific type of token) mentioned in the standard ( https://tools.ietf.org/html/rfc7519) so don't believe DRF JWT should support them. Of course I'm no authority - this is just my tuppence. :)

— Reply to this email directly or view it on GitHub https://github.com/GetBlimp/django-rest-framework-jwt/issues/92#issuecomment-149791019 .

ajostergaard commented 9 years ago

@arpheno No. :)

arpheno commented 9 years ago

My bad

On Wed, Oct 21, 2015, 09:19 AJ notifications@github.com wrote:

@arpheno https://github.com/arpheno No. :)

— Reply to this email directly or view it on GitHub https://github.com/GetBlimp/django-rest-framework-jwt/issues/92#issuecomment-149802285 .

FabrizioA commented 8 years ago

I've the same problem. How can I set the expire time of the token to 7 days or greater? I've added JWT_AUTH = {} dictionary on my djangoproject.settings module and changed the 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7) with no result. I'had also set 'JWT_VERIFY': True but the token expire in few minutes :(

tleguijt commented 8 years ago

+1 on this one. Is there a solution known already? I'm sure that this is not the intended behaviour. Why bother settings the refresh token delta in that case..

I agree with @gabn88; having the original JWT token as a refresh token doesn't make it much safer. If you have the token, but the token is expired, you can simply refresh it and you'll get a brand new one. Still, I think this behaviour should be fixed and you should be able to refresh the JWT token after the expiration date of the token itself.

guillaumevincent commented 8 years ago

The current solution requires us to use the following strategy :

The user close the app for 5 minutes > login again (this is not acceptable from a user experience point of view)

To move forward, here the section in the OAuth 2.0 RFC, talking about refreshing an Access Token :

Because refresh tokens are typically long-lasting credentials used to request additional access tokens, the refresh token is bound to the client to which it was issued.

IMO that doesn't make sense to implement refresh token but to limit them to a non-expired JWT. JWT saved on client side (localStorage or Cookie), could be refreshed by a long refresh token, even if they are expired. Refresh token should be saved on client side in a secure way.

edit: Another strategy by José F. Romaniello is to set the jwt expiration

set the jwt expiration to one week and refresh the token every time the user open the web application and every one hour. If a user doesn't open the application for more than a week, they will have to login again and this is acceptable web application UX. To refresh the token your api needs a new endpoint that receives a valid, not expired JWT and returns the same signed JWT with the new expiration field. Then the web application will store the token somewhere.

I don't know if it is very secure

anx-ckreuzberger commented 8 years ago

Hi,

1) absolutely yes on refreshing the token every time a user access the website (and if a refresh is not possible, force the user to log in) - you can also keep refreshing the token every couple of minutes while being logged in

2) Increasing the expiration to one week (one month / one year / whatever you decide is "good behaviour") seems like the best way to accomplish "staying logged in".

I have to say, however, that the wording on JWT_REFRESH_EXPIRATION_DELTA is not very precise, or the setting itself might be obsolete? (mind you, the default setting of this is 7 days !!!)

Limit on token refresh, is a datetime.timedelta instance. This is how much time after the original token that future tokens can be refreshed from.

So what does this mean? I surely can NOT refresh an already expired token (see posts in this issue), then why is this set to 7 days? Does this mean that IF I was to set the basic token expiration time to something above 7 days (for whatever reason), then I can not refresh the token if it is older than 7 days?

If so, what is the reason for this behaviour? When would I use this? When would I want to not use this?

guillaumevincent commented 8 years ago

@anx-ckreuzberger +1, the documentation is unclear on this point

This is an annoying behaviour, @all what solution do you use ? (cc @MasterKale)

MasterKale commented 8 years ago

I've been silently following along as the +1's roll in. Obviously this functionality isn't working as advertised. Either the docs need to be updated to reflect the reality of how functionality works or the functionality needs to be updated to match the docs. The former isn't exactly a solution so my vote's for the latter.

The now-closed PR #218 points to a plugin for this package that adds in refresh tokens so I don't even know anymore. Short-lived tokens plus a long-lived refresh token seem standard in JWT land, and now I'm curious to know if this package's author's original intent was to do away with the refresh token and just have a single token that pulled double-duty as a refresh token as well to generate its own replacement within N days...

In any case the obvious solution is to just use Django's built-in TokenAuth and be done with it :stuck_out_tongue:

laoyur commented 8 years ago

the document is clear but not that easy to understand in my opinion.

the default settings

'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300), 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),

means: you need to refresh token every 5 mins or you will be logged off, and even you keep on refreshing token every 5 mins, you will still be logged off in 7 days after the original token( the token you get via obtain_jwt_token api ) has been issued.

Obviously, the 5 mins + 7 days default setting is not suitable for mobile apps. In my opinion , I will set 30 days + 360 days for mobile apps. The user will keep logged in if he use my app at least once per month, and don’t need to re-enter username / password in one year.

anx-ckreuzberger commented 8 years ago

@laoyur your example makes much more sense, and the updated documentation by @guillaumevincent seems to make a clear statement that refresh is only useable for non expired tokens.

guillaumevincent commented 8 years ago

@anx-ckreuzberger sorry for the flood, github try to fix an issue. I can't make a pull request to improve the documentation for now. :(

If someone wants to improve the doc and create a PR with this commit : https://github.com/GetBlimp/django-rest-framework-jwt/commit/e833f3b9e57df289f2ef4bdd8059a38dc4c64e26

kodeine commented 8 years ago

i have the following settings,

    'JWT_ALLOW_REFRESH': True,
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),

after like 10 refreshes i get the error Signature has expired. Any idea's?

guillaumevincent commented 8 years ago

I've created a PR to help user to understand default behaviour https://github.com/GetBlimp/django-rest-framework-jwt/pull/263

angvp commented 7 years ago

I think we can close this issue now @guillaumevincent ?

guillaumevincent commented 7 years ago

@angvp +1 for me but you should ask @MasterKale first :)

angvp commented 7 years ago

@MasterKale input here?

MasterKale commented 7 years ago

Before closing this ticket out, I think you should also update the Refresh Token section of the static site: http://getblimp.github.io/django-rest-framework-jwt/#refresh-token

Right now it looks like a copy-paste of the index.md in the docs/ folder but it's missing the emphasis on "non-expired" tokens that you PR'd in a few months back.

If you guys are cool with it, I might PR an update to docs that includes some additional write-up on the interplay of JWT_EXPIRATION_DELTA and JWT_REFRESH_EXPIRATION_DELTA. Going by the default values alone I think it will still cause confusion, especially if people come to this library expecting it to work like, say, Auth0 with their issuance of non-spec "refresh tokens".

@laoyur's example was great for understanding that it's possible to set longer expiration deltas to achieve the goal of using JWTs to keep users logged in without them expiring too quickly and thus forcing the user to constantly log back in.

Other than that feel free to close out this issue.

supra28 commented 7 years ago

Ok, so now I don't understand the point of having a Refresh Token here, why no just make the jwt last 7 days and ask the user to re-login once it expires?

davesque commented 7 years ago

Figured I'd chip in here. It does seem that having separate id and refresh tokens is a wide-spread and somewhat standard practice for JWTs. However, I have to say that I like how djangorestframework-jwt does it -- namely, having a single token perform double-duty as an id and refresh token. This provides at least one of the benefits of using refresh tokens which is to allow for sliding sessions (ones that last a certain overall amount of time but can expire more quickly after some period of inactivity) and it simplifies designing client apps that interact with a JWT authenticated service. You lose the added security of sending a more powerful secret over the wire less frequently. But how much of a risk is sending any token over the wire really, so long as you're using properly configured TLS? As far as I understand, the only way such a connection could be compromised is if someone cracked it (impossible with current tech) or you are man-in-the-middled (unlikely).

Anyone care to comment on this?

psychok7 commented 7 years ago

OMG i got so lost while reading all of this (and the related PRs and issues). I am new to django-rest-framework-jwt and jwt.

I did understand this one https://github.com/GetBlimp/django-rest-framework-jwt/issues/92#issuecomment-227763338 so i am wondering if this is django-rest-framework-jwt advised approach to this problem ?

wlwg commented 7 years ago

274

michaeljpeake commented 7 years ago

From what I understand, a token 'expires' when it is older than JWT_EXPIRATION_DELTA.

You can refresh a non-expired token until it is older than JWT_REFRESH_EXPIRATION_DELTA.

So if JWT_EXPIRATION_DELTA < JWT_REFRESH_EXPIRATION_DELTA, then the JWT_REFRESH_EXPIRATION_DELTA setting is (in a practical sense) superseded by JWT_EXPIRATION_DELTA.

That is how I'ver interpreted the docs. Why are these the defaults?

JWT_EXPIRATION_DELTA = datetime.timedelta(seconds=300)
JWT_REFRESH_EXPIRATION_DELTA = datetime.timedelta(days=7)

You'll never be able to refresh after 4 days because the token will have expired! Is there any point to the JWT_REFRESH_EXPIRATION_DELTA setting?

Or ... Is it that there is part of the JWT signature that does not change when you refresh it? Is it the case that you get your first token, then continue to refresh every five minutes (extending the use of that signature), and can continue to do that for seven days. Then, the first time you can that refresh after seven days, you are forced to get a brand new token?

If this is the case, I think the documentation should make this clear. This is, in effect, allowing a user session to last up to seven days, so long as the token is refreshed every five minutes. Why not say that?

python-programmer commented 6 years ago

All that we want is:

request with a expired token and refresh token:

curl -X POST -H 'Authorization: bearer dGVzdGNsaWVudDpzZWNyZXQ=' -d 'refresh_token=fdb8fdbecf1d03ce5e6125c067733c0d51de209c&grant_type=refresh_token' localhost:8000/auth/refresh_token/

and response:

`{

"token_type":"bearer",
"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiVlx1MDAxNcKbwoNUwoonbFPCu8KhwrYiLCJpYXQiOjE0NDQyNjI4NjYsImV4cCI6MTQ0NDI2Mjg4Nn0.Dww7TC-d0teDAgsmKHw7bhF2THNichsE6rVJq9xu_2s",
"expires_in":20,
"refresh_token":"7fd15938c823cf58e78019bea2af142f9449696a"

}`

thank you in advance

danidee10 commented 6 years ago

I'm having a weird issue. my settings:

JWT_ALLOW_REFRESH = True
JWT_REFRESH_DELTA = datetime.timedelta(minutes=1)

I didn't set JWT_REFRESH_EXPIRATION_DELTA Which means it's 7 days (The default).

From my understanding, it means that my token expires after 1 minute, but I'm allowed to refresh the token before that time for 7 days. After 7 days, the token expires and I have to log out and login again to obtain a brand new token.

On the client side, I refreshed every 30 seconds. The docs say you'll obtain a "brand new token" though I still got the same token (That's not a problem).

After REFRESHING 10 TIMES The token expires and the endpoint returns 400. This is the log from Django (/xxx/xxxx is the refresh URL)

"POST /jwt/create/ HTTP/1.1" 200 207  # Login and obtain a new token
"OPTIONS /xxx/xxxx/ HTTP/1.1" 200 0  # I have no idea why this happens :-)
"POST /xxx/xxxx/ HTTP/1.1" 200 207
"POST /xxx/xxxx/ HTTP/1.1" 200 207
"POST /xxx/xxxx/ HTTP/1.1" 200 207
"POST /xxx/xxxx/ HTTP/1.1" 200 207
"POST /xxx/xxxx/ HTTP/1.1" 200 207
"POST /xxx/xxxx/ HTTP/1.1" 200 207
"POST /xxx/xxxx/ HTTP/1.1" 200 207
"POST /xxx/xxxx/ HTTP/1.1" 200 207
"POST /xxx/xxxx/ HTTP/1.1" 200 207
"POST /xxx/xxxx/ HTTP/1.1" 400 47  # Error starts here

Exactly 10 times (disregard the OPTIONS request) as @kodeine reported.

guillaumevincent commented 6 years ago

@danidee10 there is no JWT_REFRESH_DELTA See the doc http://getblimp.github.io/django-rest-framework-jwt/#refresh-token

By default JWT_EXPIRATION_DELTA is 300 seconds

And also

If JWT_ALLOW_REFRESH is True, non-expired tokens can be "refreshed" to obtain a brand new token with renewed expiration time.

So you cannot refresh your token after 10 x 30s.

superandrew commented 6 years ago

+1

Why is this issue still open? I don't get what was the final decision. It seems that the current behaviour is that the JWT_REFRESH_EXPIRATION_DELTA should be < than JWT_EXPIRATION_DELTA. Is that so?

In this case my opinion is that this is not following the general JWT pattern, but the most sure thing is that I second @michaeljpeake regarding the confusing default values for the settings.

superandrew commented 6 years ago

for anyone who needs this feature, found this framework that behaves exactly as expected https://github.com/davesque/django-rest-framework-simplejwt. I hope it could help.

iicdii commented 5 years ago

For anyone who will use django-rest-auth with django-rest-framework-simple-jwt, it is not supported yet. Here(https://github.com/Tivix/django-rest-auth/issues/430) you can find an issue about supporting simple-jwt.