Hi, I'm trying to use sliding tokens, but I think I need a bit of clarification. From what I understand, when I generate a sliding token, it can be used for authentication until it's expiration claim expires. After the expiration claim expires, I can still refresh the token until the refresh expiration claim expires.
The problem I'm running into is that I can only refresh the token while the auth expiration claim is valid. I'm not sure if this is intended or if I'm doing something wrong, but this makes it seem like there's no point to the refresh expiration claim.
My config is:
SIMPLE_JWT = {
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.SlidingToken',),
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=1),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
I can obtain a token:
Request:
> POST /api/users/token/ HTTP/1.1
> Host: localhost:8000
> User-Agent: insomnia/6.6.2
> Content-Type: application/json
> Accept: */*
> Content-Length: 44
| {
| "username": "test",
| "password": "test"
| }
< HTTP/1.1 200 OK
< Date: Thu, 29 Aug 2019 16:56:17 GMT
< Server: WSGIServer/0.2 CPython/3.7.4
< Content-Type: application/json
< Vary: Accept
< Allow: POST, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Content-Length: 252
| {
| "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoic2xpZGluZyIsImV4cCI6MTU2NzA5NzgzNywianRpIjoiOWZkZDFkYTlkODI2NDAwNzkwNjRiYjM4Nzc5Y2FkMmQiLCJyZWZyZXNoX2V4cCI6MTU2NzE4NDE3NywidXNlcl9pZCI6Mn0.1eb5oFz6wwyjBI1wQrl5tPkkUogXWmWBBTWJn_xJC2k"
| }
I can use that token for authentication:
> GET /api/post/1 HTTP/1.1
> Host: localhost:8000
> User-Agent: insomnia/6.6.2
> Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoic2xpZGluZyIsImV4cCI6MTU2NzA5NzgzNywianRpIjoiOWZkZDFkYTlkODI2NDAwNzkwNjRiYjM4Nzc5Y2FkMmQiLCJyZWZyZXNoX2V4cCI6MTU2NzE4NDE3NywidXNlcl9pZCI6Mn0.1eb5oFz6wwyjBI1wQrl5tPkkUogXWmWBBTWJn_xJC2k
> Accept: */*
< HTTP/1.1 200 OK
< Date: Thu, 29 Aug 2019 16:57:06 GMT
< Server: WSGIServer/0.2 CPython/3.7.4
< Content-Type: application/json
< Vary: Accept
< Allow: GET, POST, HEAD, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Content-Length: 140
| [correct response data...]
After SLIDING_TOKEN_LIFETIME (1 minute), trying to make authenticated requests gives a 401, which I would expect:
> GET /api/post/1 HTTP/1.1
> Host: localhost:8000
> User-Agent: insomnia/6.6.2
> Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoic2xpZGluZyIsImV4cCI6MTU2NzA5NzgzNywianRpIjoiOWZkZDFkYTlkODI2NDAwNzkwNjRiYjM4Nzc5Y2FkMmQiLCJyZWZyZXNoX2V4cCI6MTU2NzE4NDE3NywidXNlcl9pZCI6Mn0.1eb5oFz6wwyjBI1wQrl5tPkkUogXWmWBBTWJn_xJC2k
> Accept: */*
< HTTP/1.1 401 Unauthorized
< Date: Thu, 29 Aug 2019 17:00:27 GMT
< Server: WSGIServer/0.2 CPython/3.7.4
< Content-Type: application/json
< WWW-Authenticate: Bearer realm="api"
< Vary: Accept
< Allow: GET, POST, HEAD, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Content-Length: 185
| {
| "detail": "Given token not valid for any token type",
| "code": "token_not_valid",
| "messages": [
| {
| "token_class": "SlidingToken",
| "token_type": "sliding",
| "message": "Token is invalid or expired"
| }
| ]
| }
But when I try to refresh the token within SLIDING_TOKEN_REFRESH_LIFETIME:
> POST /api/users/token/refresh/ HTTP/1.1
> Host: localhost:8000
> User-Agent: insomnia/6.6.2
> Content-Type: application/json
> Accept: */*
> Content-Length: 256
| {
| "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoic2xpZGluZyIsImV4cCI6MTU2NzA5NzgzNywianRpIjoiOWZkZDFkYTlkODI2NDAwNzkwNjRiYjM4Nzc5Y2FkMmQiLCJyZWZyZXNoX2V4cCI6MTU2NzE4NDE3NywidXNlcl9pZCI6Mn0.1eb5oFz6wwyjBI1wQrl5tPkkUogXWmWBBTWJn_xJC2k"
| }
< HTTP/1.1 401 Unauthorized
< Date: Thu, 29 Aug 2019 17:02:53 GMT
< Server: WSGIServer/0.2 CPython/3.7.4
< Content-Type: application/json
< WWW-Authenticate: Bearer realm="api"
< Vary: Accept
< Allow: POST, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Content-Length: 65
| {
| "detail": "Token is invalid or expired",
| "code": "token_not_valid"
| }
But if I try refreshing a token within SLIDING_TOKEN_LIFETIME, it works fine:
> POST /api/users/token/refresh/ HTTP/1.1
> Host: localhost:8000
> User-Agent: insomnia/6.6.2
> Content-Type: application/json
> Accept: */*
> Content-Length: 256
| {
| "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoic2xpZGluZyIsImV4cCI6MTU2NzA5ODM1NSwianRpIjoiNTg5ODhiMGFkMWE1NGU3MWFkOGU2NTMxZGI4ZTNhMTAiLCJyZWZyZXNoX2V4cCI6MTU2NzE4NDY5NSwidXNlcl9pZCI6Mn0.BW-qxTVupUlBogT4O-s_ySKjEdfPkl_zX7vw-d903iA"
| }
< HTTP/1.1 200 OK
< Date: Thu, 29 Aug 2019 17:05:06 GMT
< Server: WSGIServer/0.2 CPython/3.7.4
< Content-Type: application/json
< Vary: Accept
< Allow: POST, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Content-Length: 252
| {
| "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoic2xpZGluZyIsImV4cCI6MTU2NzA5ODM2NiwianRpIjoiNTg5ODhiMGFkMWE1NGU3MWFkOGU2NTMxZGI4ZTNhMTAiLCJyZWZyZXNoX2V4cCI6MTU2NzE4NDY5NSwidXNlcl9pZCI6Mn0.nnBpI7FDiFZOa-f4fGQC5omtff2g6WFg5NKVlE6k0CM"
| }
So am I doing something wrong? What is SLIDING_TOKEN_REFRESH_LIFETIME for? The way I expected to use a sliding token would be to retrieve a token on login, and use it for requests. Once a request returns a 401 telling me the token expired, I refresh it to obtain a new token, and repeat the process.
Thanks! Aside from refreshing sliding tokens, this library has been super easy to use :)
@IvanFon were you able to solve this?
No, unfortunately. I ended up using separate auth and refresh tokens, which works fine.
It鈥檚 not a huge hassle to use two separate tokens, but in my case it would be slightly more convenient to use a single sliding token. Having separate tokens wouldn鈥檛 improve my security, both tokens are stored in memory/localStorage. If an attacker gets access to one, they have access to both.
On October 17, 2019 6:06:04 a.m. EDT, BlackXnt notifications@github.com wrote:
@IvanFon were you able to solve this?
--
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub:
https://github.com/davesque/django-rest-framework-simplejwt/issues/154#issuecomment-543103887
I think I'm having the same problem...
If I understand the problem correctly, in the time after exp and before refresh_exp, we should be able to refresh the token with post('/api/tokens/refresh/', {'token': token}). But instead, we're getting 401 errors.
So I dug into the source a little to see if I could see what's going on.
A low-level verification function is checking if exp has passed (which it has) instead of refresh_exp (which hasn't). Because exp has passed, the functions are throwing the token out.
Here's a nice, long stack-trace getting to the bottom of what's happening...
If you're interested like I was, grab a coffee and read on.
We'll start at the TokenRefreshSlidingSerializer, called with the refresh api, which looks like this:
class TokenRefreshSlidingSerializer(serializers.Serializer):
token = serializers.CharField()
def validate(self, attrs):
token = SlidingToken(attrs['token'])
# Check that the timestamp in the "refresh_exp" claim has not
# passed
token.check_exp(api_settings.SLIDING_TOKEN_REFRESH_EXP_CLAIM)
# Update the "exp" claim
token.set_exp()
return {'token': str(token)}
The token gets rejected at the token = SlidingToken(attrs['token']) line, so I checked the SlidingToken.__init__ function, next:
class SlidingToken(BlacklistMixin, Token):
token_type = 'sliding'
lifetime = api_settings.SLIDING_TOKEN_LIFETIME
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.token is None:
# Set sliding refresh expiration claim if new token
self.set_exp(
api_settings.SLIDING_TOKEN_REFRESH_EXP_CLAIM,
from_time=self.current_time,
lifetime=api_settings.SLIDING_TOKEN_REFRESH_LIFETIME,
)
Killed at the super(), so next stop, Token.__init__.
def __init__(self, token=None, verify=True):
"""
!!!! IMPORTANT !!!! MUST raise a TokenError with a user-facing error
message if the given token is invalid, expired, or otherwise not safe
to use.
"""
if self.token_type is None or self.lifetime is None:
raise TokenError(_('Cannot create token with no type or lifetime'))
self.token = token
self.current_time = aware_utcnow()
# Set up token
if token is not None:
# An encoded token was provided
from .state import token_backend
# Decode token
try:
self.payload = token_backend.decode(token, verify=verify)
except TokenBackendError:
raise TokenError(_('Token is invalid or expired'))
if verify:
self.verify()
else:
# New token. Skip all the verification steps.
self.payload = {api_settings.TOKEN_TYPE_CLAIM: self.token_type}
# Set "exp" claim with default value
self.set_exp(from_time=self.current_time, lifetime=self.lifetime)
# Set "jti" claim
self.set_jti()
self.payload = token_backend.decode(token, verify=verify) is the line where this dies. And what happens in there?
def decode(self, token, verify=True):
"""
Performs a validation of the given token and returns its payload
dictionary.
Raises a `TokenBackendError` if the token is malformed, if its
signature check fails, or if its 'exp' claim indicates it has expired.
"""
try:
return jwt.decode(token, self.verifying_key, algorithms=[self.algorithm], verify=verify)
except InvalidTokenError:
raise TokenBackendError(_('Token is invalid or expired'))
Yup that's right, it's only checking the exp key, not the refresh_exp key.
Just to be thorough, let's see what's going on in the jwt library.
Because verify=True, jwt.decode goes ahead and calls an internal function called self._validate_claims, which finally calls self._validate_exp, which, of course, gives us a nice ExpiredSignatureError.
To fix this, one could change the SlidingToken initialization in the TokenRefreshSlidingSerializer from
token = SlidingToken(attrs['token'])
to
token = SlidingToken(attrs['token'], verify=False)
which I successfully tested, but I don't know the library well enough to understand the ramifications of that kind of action.
I hope this helps someone, somewhere?
Yeah, I included the whole sliding token thing to provide some kind of backwards compatibility with users of the legacy django-rest-framework-jwt library. But I honestly just don't like the idea and I kinda want to get rid of it. I need to look into how easy it would be to do this.
An alternative, as suggested @IvanFon is to use pair token. Dont forget to set 'ROTATE_REFRESH_TOKENS' to True, if you decide to go for the pair token approach. I forgot this flag existed which made me believe sliding tokens was the only alternative.
Just wanted to add that I've also stumbled upon this issue. Thanks @decepulis for digging into the issue and finding a workaround in the meantime.
I bumped in to this issue aswell since I wanted a fault proof system where only one valid token was present for a user. When using a pair, the access token is still valid for a certain time even though the refresh has been blacklisted. One additional step to add is that when overriding the serializer, you should also add token.check_blacklist() and token.blacklist() to make old tokens invalid and token.set_jti() for creating a new token.
I think I'm having the same problem...
If I understand the problem correctly, in the time afterexpand beforerefresh_exp, we should be able to refresh the token withpost('/api/tokens/refresh/', {'token': token}). But instead, we're getting 401 errors.So I dug into the source a little to see if I could see what's going on.
TL;DR
A low-level verification function is checking if
exphas passed (which it has) instead ofrefresh_exp(which hasn't). Becauseexphas passed, the functions are throwing the token out.What Goes Wrong
Here's a nice, long stack-trace getting to the bottom of what's happening...
If you're interested like I was, grab a coffee and read on.We'll start at the TokenRefreshSlidingSerializer, called with the refresh api, which looks like this:
class TokenRefreshSlidingSerializer(serializers.Serializer): token = serializers.CharField() def validate(self, attrs): token = SlidingToken(attrs['token']) # Check that the timestamp in the "refresh_exp" claim has not # passed token.check_exp(api_settings.SLIDING_TOKEN_REFRESH_EXP_CLAIM) # Update the "exp" claim token.set_exp() return {'token': str(token)}The token gets rejected at the
token = SlidingToken(attrs['token'])line, so I checked theSlidingToken.__init__function, next:class SlidingToken(BlacklistMixin, Token): token_type = 'sliding' lifetime = api_settings.SLIDING_TOKEN_LIFETIME def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.token is None: # Set sliding refresh expiration claim if new token self.set_exp( api_settings.SLIDING_TOKEN_REFRESH_EXP_CLAIM, from_time=self.current_time, lifetime=api_settings.SLIDING_TOKEN_REFRESH_LIFETIME, )Killed at the
super(), so next stop,Token.__init__.def __init__(self, token=None, verify=True): """ !!!! IMPORTANT !!!! MUST raise a TokenError with a user-facing error message if the given token is invalid, expired, or otherwise not safe to use. """ if self.token_type is None or self.lifetime is None: raise TokenError(_('Cannot create token with no type or lifetime')) self.token = token self.current_time = aware_utcnow() # Set up token if token is not None: # An encoded token was provided from .state import token_backend # Decode token try: self.payload = token_backend.decode(token, verify=verify) except TokenBackendError: raise TokenError(_('Token is invalid or expired')) if verify: self.verify() else: # New token. Skip all the verification steps. self.payload = {api_settings.TOKEN_TYPE_CLAIM: self.token_type} # Set "exp" claim with default value self.set_exp(from_time=self.current_time, lifetime=self.lifetime) # Set "jti" claim self.set_jti()
self.payload = token_backend.decode(token, verify=verify)is the line where this dies. And what happens in there?def decode(self, token, verify=True): """ Performs a validation of the given token and returns its payload dictionary. Raises a `TokenBackendError` if the token is malformed, if its signature check fails, or if its 'exp' claim indicates it has expired. """ try: return jwt.decode(token, self.verifying_key, algorithms=[self.algorithm], verify=verify) except InvalidTokenError: raise TokenBackendError(_('Token is invalid or expired'))Yup that's right, it's only checking the exp key, not the refresh_exp key.
Just to be thorough, let's see what's going on in the jwt library.
Becauseverify=True, jwt.decode goes ahead and calls an internal function calledself._validate_claims, which finally callsself._validate_exp, which, of course, gives us a niceExpiredSignatureError.A Brute-Force Solution
To fix this, one could change the SlidingToken initialization in the
TokenRefreshSlidingSerializerfromtoken = SlidingToken(attrs['token'])to
token = SlidingToken(attrs['token'], verify=False)which I successfully tested, but I don't know the library well enough to understand the ramifications of that kind of action.
I hope this helps someone, somewhere?
I hope this helps someone, somewhere?
Have a security error in this solution. With verify=False on SlidingToken, the signature validate is don't verified, an is possible change the payload. Is possible make a test of this on debbuger of https://jwt.io/, changing the payload, with wrong key, and submit to refresh.
@Kaspary You're probably using PyJWT 2.0.0. Ref #349 Once it's merged, it should solve your problem. Until then, downgrade to PyJWT 1.7.1. Sorry for the inconvenience!
one doubt, how do i create a sliding Token??
Please don't use sliding tokens. They're confusing. (They came from migrating from drf-jwt). We may be deprecating is soon as migration from drf-jwt is much lower than before.
ok thanks @Andrew-Chen-Wang
Most helpful comment
I think I'm having the same problem...
If I understand the problem correctly, in the time after
expand beforerefresh_exp, we should be able to refresh the token withpost('/api/tokens/refresh/', {'token': token}). But instead, we're getting 401 errors.So I dug into the source a little to see if I could see what's going on.
TL;DR
A low-level verification function is checking if
exphas passed (which it has) instead ofrefresh_exp(which hasn't). Becauseexphas passed, the functions are throwing the token out.What Goes Wrong
Here's a nice, long stack-trace getting to the bottom of what's happening...
If you're interested like I was, grab a coffee and read on.
We'll start at the TokenRefreshSlidingSerializer, called with the refresh api, which looks like this:
The token gets rejected at the
token = SlidingToken(attrs['token'])line, so I checked theSlidingToken.__init__function, next:Killed at the
super(), so next stop,Token.__init__.self.payload = token_backend.decode(token, verify=verify)is the line where this dies. And what happens in there?Yup that's right, it's only checking the exp key, not the refresh_exp key.
Just to be thorough, let's see what's going on in the jwt library.
Because
verify=True, jwt.decode goes ahead and calls an internal function calledself._validate_claims, which finally callsself._validate_exp, which, of course, gives us a niceExpiredSignatureError.A Brute-Force Solution
To fix this, one could change the SlidingToken initialization in the
TokenRefreshSlidingSerializerfromto
which I successfully tested, but I don't know the library well enough to understand the ramifications of that kind of action.
I hope this helps someone, somewhere?