How to revoke refresh token on password reset? - django

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

Related

How to blacklist a JWT token with Simple JWT (django rest)?

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,

How to disable a user's password in AWS using boto3

I am auditing user passwords in AWS using boto3 and I'm not finding a way to accomplish the following CIS Benchmark: "Ensure credentials (with password enabled) unused for 90 days or greater are disabled."
I have the code to pull the password age and to pull the last time the password was used, but I do not find anything to make inactive a password.
For access keys (but not passwords), we have the following:
client = session.client('iam')
... (get user and keyid) ...
last_used = client.get_access_key_last_used(AccessKeyId=keyid)
... (determine the age of the key) ...
if age >= 90:
client.update_access_key(AccessKeyId=keyid, Status='Inactive', UserName=user)
Does anyone have any pointers?
delete_login_profile is the one you should use if you want to delete the password for the specified IAM user, which terminates the user's ability to access AWS services through the AWS Management Console.
However to prevent all user access (including CLI and API access) you must also either make any access keys inactive or delete them.
From Boto3 Documentation:
Warning
Deleting a user's password does not prevent a user from accessing AWS
through the command line interface or the API. To prevent all user
access you must also either make any access keys inactive or delete
them. For more information about making keys inactive or deleting
them, see UpdateAccessKey and DeleteAccessKey.
If you want to change the password, you should use update_login_profile boto3 API. If you want to disable the password, you need to use delete_login_profile.
boto3 documentation for update_login_profile can be found here.
boto3 documentation for delete_login_profile can be found here.
Thanks to the responders, delete_login_profile followed by a password reset using create_login_profile is exactly what I needed. I saw it in the docs, but "delete" just sounded too scary.
def getPassword(client, user):
''' get the password data from aws '''
try:
response = client.get_login_profile(UserName=user)
return response
except client.exceptions.NoSuchEntityException as e:
print(e)
return ''
# setup the client handler
client = session.client('iam')
# set the user
user = 'some.user'
# if the user has a password, execute this code block
if getPassword(client=client, user=user):
... code to test the password age here ...
... if it's too old, then ...
# remove the login_profile/password/ability to use the Console
client.delete_login_profile(UserName=user)
# set the new password
passwd = raw_input('Enter New Password: ')
# create the new login_profile with the new password and force the user to change the password on the next login
client.create_login_profile(UserName=user, Password=passwd, PasswordResetRequired=True)

Revoking tokens using Django rest-framework-jwt

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.

How does timed JSON web signature serializer work?

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)

Logout or end session using itsdangerous signing

If I create a token for my API like this:
def generate_auth_token(self, **kwargs):
s = Serializer(app.config['SECRET_KEY'], expires_in = 172800)
return s.dumps({ 'id': kwargs['user_id'] })
How can I end a user's session?
You can't if that's the only information in the token. You can solve this by adding more information to the token payload, and possibly storing extra information about valid tokens server side.
For example, you could store a hash of the email and password, so if either changes, the hash will not compare anymore
from hashlib import md5
from werkzeug.security import safe_str_cmp
# generate the hash and store it in the token along with the id
hash = md5('{}{}'.format(user.email, user.password).encode('utf8')).hexdigest()
###
# load the user from the id in the token and generate its hash
hash = md5('{}{}'.format(user.email, user.password).encode('utf8')).hexdigest()
# then compare the hash in the token to the hash for the user
if not safe_str_cmp(hash, token['hash']):
# don't log in user
This is done in a hash rather than just including the email and password directly because the token is signed but not encrypted.
If you want to be able to invalidate all tokens without changing the email or password, you can store some random key per user and add that to the hash. Generating a new random key will invalidate all previous tokens.
You could also take this even further and just store the full tokens server side, removing them on logout so they can't be used again.