I'm thinking of allowing a user to revoke previously issued tokens (yes, even though they are set to expire in 15 minutes), but did not find any way to do so using DRF-jwt.
Right now, I'm considering several options:
Hope someone on SO will show me how to do this out-of-the-box ;-)
Use the jti field as a counter, and, upon revocation, require jti > last jti.
Add user-level salt to the signing procedure, and change it upon revocation
Store live tokens in some Redis DB
Is any of the above the way to go?
We did it this way in our project:
Add jwt_issue_dt to User model.
Add original_iat to payload. So token refresh won't modify this field.
Compare original_iat from payload and user.jwt_issue_dt:
from calendar import timegm
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class CustomJSONWebTokenAuthentication(JSONWebTokenAuthentication):
def authenticate_credentials(self, payload):
user = super(CustomJSONWebTokenAuthentication, self).authenticate_credentials(payload)
iat_timestamp = timegm(user.jwt_issue_dt.utctimetuple())
if iat_timestamp != payload['iat']:
raise exceptions.AuthenticationFailed('Invalid payload')
return user
To revoke a token you just need to update the field user.jwt_issue_dt.
Related
I'm using Simple JWT to use JWT tokens in my Django rest API. It works great but I would like to be able to blacklist a token when a user logs out. In the documentation, it is said:
If the blacklist app is detected in INSTALLED_APPS, Simple JWT will add any generated refresh or sliding tokens to a list of outstanding tokens. It will also check that any refresh or sliding token does not appear in a blacklist of tokens before it considers it as valid. The Simple JWT blacklist app implements its outstanding and blacklisted token lists using two models: OutstandingToken and BlacklistedToken. Model admins are defined for both of these models. To add a token to the blacklist, find its corresponding OutstandingToken record in the admin and use the admin again to create a BlacklistedToken record that points to the OutstandingToken record.
However, I didn't find any code example and I'm not sure how this should be implemented. An example would be greatly appreciated.
Simple JWT only blacklists refresh tokens. This can be done by setting:
INSTALLED_APPS = (
...
'rest_framework_simplejwt.token_blacklist',
...
}
and then running migrate.
So, i would suggest, inorder to logout user:
Delete both, refresh & access tokens from the client. Also, keep access token expiry as short as possible.
Black-list the refresh token by creating an api end-point.
urls.py
path('/api/logout', views.BlacklistRefreshView.as_view(), name="logout"),
views.py
from rest_framework_simplejwt.tokens import RefreshToken
class BlacklistRefreshView(APIView):
def post(self, request)
token = RefreshToken(request.data.get('refresh'))
token.blacklist()
return Response("Success")
This will make sure that the refresh token cannot be used again to generate a new token (if at all someone has acquired it). Also, since access token has short life, it will be invalidated soon hopefully.
Not sure about this, but it's works for me to use token.blacklist().
make tokens.py
from rest_framework_simplejwt.tokens import AccessToken, BlacklistMixin
class JWTAccessToken(BlacklistMixin, AccessToken):
pass
in settings.py
SIMPLE_JWT = {
...
'AUTH_TOKEN_CLASSES': ('path_to_tokens_py.tokens.JWTAccessToken',),
...
}
I was getting the same error:
TokenError(_('Token is invalid or expired'))
because of passing the access token in:
token = RefreshToken(access_token)
while I should pass in the refresh token.
We can blacklist refresh tokens I would use redis with key pair token[refresh]=access and check for refresh token with blacklist endpoint
path('token/blacklist/', TokenBlacklistView.as_view(), name='token_blacklist'),
Send a refresh token
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
verify your token you would send refresh token.
Use an in-memory database to check blacklist refreshed token
now you can make your end point to check the access list token dont forget to add the following lines in settings.py
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
I am using django-rest-framework-simplejwt to get access token and refresh token .
The problem is that refresh token is not becoming invalid if I change the password of the user. Basically I can continue to send refresh token and get new access tokens even after user has changed password.
What I would like instead is to ask user to re-submit the username and new password to get a new pair of access and refresh tokens.
How would I accomplish this?
PS: Just because I am curious, shouldn't this be the default behaviour of the library? In what case would we want to retain the refresh token after credentials have changed?
I figured how to get this working.
What I am did is put a signal that tracks if any required parameter has changed. If so, it blacklists all the refresh tokens associated with that user.
Here is the code:
First add 'rest_framework_simplejwt.token_blacklist' in installed apps. Then:
#receiver(signals.pre_save, sender=User)
def revoke_tokens(sender, instance, update_fields, **kwargs):
if not instance._state.adding: #instance._state.adding gives true if object is being created for the first time
existing_user = User.objects.get(pk=instance.pk)
if instance.password != existing_user.password or instance.email != existing_user.email or instance.username != existing_user.username:
# If any of these params have changed, blacklist the tokens
outstanding_tokens = OutstandingToken.objects.filter(user__pk=instance.pk)
# Not checking for expiry date as cron is supposed to flush the expired tokens
# using manage.py flushexpiredtokens. But if You are not using cron,
# then you can add another filter that expiry_date__gt=datetime.datetime.now()
for out_token in outstanding_tokens:
if hasattr(out_token, 'blacklistedtoken'):
# Token already blacklisted. Skip
continue
BlacklistedToken.objects.create(token=out_token)
WHat this code basically does is , gets all outstanding tokens for the user, then adds all of them to blacklist. You can get more info on outstanding/blacklisted tokens here.
https://github.com/davesque/django-rest-framework-simplejwt#blacklist-app
I am playing around with Flask. I like that it is fairly thin and works for most of my requirements.
I would like to know what is your recommended way of retrieving the current logged in user. I would like every HTTP request, which is made, to pass/carry a token in the header, which is first retrieved by the login api
/user/login (params: username, password)
# returns {success: True, token: "<some-unique-string>"
Now is the subsequent APIs I would like to get the user object from the from passed token, like so
#app.route("/user/info", methods = ["GET"])
#apify
def user_get_info():
return {"name": current_user().name}
How could I have current_user read from the header without having to pass the request object every time ?
Any thoughts?
Every token is stored against a user. So you can make a query to get a user against a given token. Something like:
token = Token.query.get(token='token_value')
return jsonify({'name: User.query.get(id=token.user_id).name})
This can be done in one line as well but it totally depends upon your models and relationships.
Hope that helps. If I haven't understood your question correctly, do elaborate a little.
Can I restrict actions of my API to specific users if I generate a token like this:
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
expiration = 600
s = Serializer(current_app.config['SECRET_KEY'], expires_in = expiration)
return s.dumps({ 'id': kwargs.get('user_id') })
And the verification
#staticmethod
def verify_auth_token(token):
s = Serializer(app.config['SECRET_KEY'])
try:
data = s.loads(token)
except SignatureExpired:
return None # valid token, but expired
except BadSignature:
return None # invalid token
user = User.query.get(data['id'])
return user
I don't understand how this works and achieves security. The way I'm used to securing an API for example, a user wants to do HTTP PUT to /posts/10 I'd usually get the post's author ie user_id then query the database get the token for that user_id, if the request token matches the queried token then it is safe for the PUT. I've read this article and don't fully understand how it achieves security without storing anything in a database. Could someone explain how it works?
By signing and sending the original token upon login the server basically gives the front end an all access ticket to the data the user would have access to, and the front end uses that token (golden ticket) on all future requests for as long as the token is not expired (tokens can be made to have expiration or not). The server in turn knows the token has not been tampered with, because the signature is basically the encrypted hash of the users recognizable data (user_id, username, etc). So, if you change the token information from something like:
{"user_id": 1}
to something like:
{"user_id": 2}
then the signature would be different and the server immediately knows this token is invalid.
This provides an authentication method that exempts the server from having to have a session, because it validates the token every time.
Here is an example of what a token could look like (itsdangerous can use this format of JSON web tokens)
An anonymous user (no login) gets directed to a landing page where there is a button and text field to post information.
I want the act posting of data to be tied to the person landing on the page. ie knowing the target url of the post shouldn't allow you to post stuff, it should be tied to a very short duration session.
I am using Django.
What is the simplest or built in method to use?
Sessions
You can store this information in the anonymous user's session if you have a session store configured. To start the session:
request.session["allow_post_until"] = datetime.datetime.now() + datetime.timedelta(...)
And to check it:
if not (request.session["allow_post_until"] and request.session["allow_post_until"] < datetime.datetime.now()):
raise PermissionDenied
Signed Cookies
If you are using django 1.4 and don't want to configure a session store you can use signed cookies for this. When you want to enable a session for the user, set a cookie with an appropriate max_age. When a user posts, check for the signed cookie and check its validity. To set:
response.set_signed_cookie("mysession", "sessiondata", max_age=<session period in seconds>)
To check:
request.get_signed_cookie("mysession", max_age=<session period in seconds>)