DRF Handling Password Reset Email - django-rest-framework

I am new to Django Rest Framework and working on a DRF & React Authentication system. Everything is working fine except password reset. The issue I have is when the user requests a password reset a reset URL with the generated token is fired to the registered email. So the user can click the link with the embedded token and go back to the calling (or any other) URL.
http://127.0.0.1:8000/api/auth/password_reset/validate_token/?token=wBBAIfQHzTJVz9bKPLkIgB
The problem is how do I generate the view or serializer to handle the URL with the token on the fly. I just need to understand the full flow I am working with "django-rest-passwordreset1.2.1", (https://pypi.org/project/django-rest-passwordreset/) package so I am generating the reset email and token with the model which as per documentation fires a signal to generate the token and send an email. The endpoints that come with the documentation validate and work fine in postman testing. I am just not sure how to create that functionality in Django. Any help or links to a good tutorial will be appreciated.
Many thanks in advance
models.py
from django.dispatch import receiver
from django.urls import reverse
from django_rest_passwordreset.signals import reset_password_token_created
from django.core.mail import send_mail
#receiver(reset_password_token_created)
def password_reset_token_created(sender, instance, reset_password_token, *args,
**kwargs):
site = 'http://127.0.0.1:3000'
email_plaintext_message = "{}?token={}".format(reverse('password_reset:reset-password-validate'), reset_password_token.key)
reset_url = site + email_plaintext_message
send_mail(
"Password Reset for {title}".format(title="Some website title"),
reset_url,
"noreply#somehost.local",
[reset_password_token.user.email]
)
urls.py
urlpatterns = [
path('api/auth', include('knox.urls')),
path('api/auth/register', RegisterAPI.as_view()),
path('api/auth/login', LoginAPI.as_view()),
path('api/auth/user', UserAPI.as_view()),
path('api/auth/logout', knox_views.LogoutView.as_view(), name="knox_logout"),
path('api/auth/change-password/', ChangePasswordView.as_view(), name='change-password')
path('api/auth/password_reset/', include('django_rest_passwordreset.urls')),
path('admin/', admin.site.urls),
]
The include('django_rest_passwordreset.urls')) provides the following url's:
POST ${API_URL}/ -
request a reset password token by using the email parameter*
POST ${API_URL}/confirm/ -
using a valid token, the users password is set to the provided password
POST ${API_URL}/validate_token/ -
will return a 200 if a given token is valid
where ${API_URL}/ is the url specified in your urls.py (e.g., api/auth/password_reset/ as in the example above)*

OK, let's abstract from libs, and get proper steps to make it manually.
As far as I've got, you have some token token=wBBAIfQHzTJVz9bKPLkIgB that somehow linked with the User instance, am I right?
So we need to write a view that allows us to make auth and get a request with our User and then we can check the token and make a logout.
from django.contrib.auth import logout
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
#api_view["GET"]
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
def logout(request):
if request.query_params.get("token"):
if Token.objects.filter(token=request.query_params["token"]).exists()
logout(request)
# other logic is here
return Response({"status": "logged out"})
return Response({"error": "..."})
Point your urls.py file to use this API view in the needed path.
So my assumption here is that you are using something like TokenAuthentication you can change the decorator's parameter to your actual value. The same thing is for the permission_classes decorator.

The problem was with the front end, I was able to modify the parameters in the model that generated and stored the token by replacing the URL. I changed
email_plaintext_message = "{}?token={}".format(reverse('password_reset:reset-password-validate'), reset_password_token.key)
to the following
email_plaintext_message = "{}?token={}".format('http://127.0.0.1:3000/newpassword' , reset_password_token.key)
Now the return URL is http://127.0.0.1:3000/newpassword?token=f109f93a83040895e3a8b1b8afd
pointing to the correct component in the fornt-end. I then extract the token from the URL and pass it for validation along with the new password in the form hitting the required endpoint.

Related

How does Djoser account verification system really works under the hood?

So I'm currently in an attempt to make my own account verification system and I'm using some parts of Djoser as a reference. let me try to walk you to my question
Let's say you're to make a new account in Djoser app
you put in the information of your soon to be made account including email
submit the form to the backend
get an email to the whatever email account you put in earlier to verify your account
click the link in your email
get to the verify account page
now in this page there's a button to submit a UID and a token and both of those information lies in the URL.
My question is:
What are those tokens? is it JWT?
How do they work?
How can I implement that in my own projects without djoser?
The answers to your questions are immersed in the own code of djoser.
You can check djoser.email file and in the classes there, they are few methods get_context_data().
def get_context_data(self):
context = super().get_context_data()
user = context.get("user")
context["uid"] = utils.encode_uid(user.pk)
context["token"] = default_token_generator.make_token(user)
context["url"] = settings.ACTIVATION_URL.format(**context)
return context
So get the context in the class where is instance, and in this context add the 'uid' (this is basically str(pk) and coded in base64, check encode_uid()), the 'token' (just a random string created with a Django function from your Secret key; you can change the algorithm of that function and the duration of this token with PASSWORD_RESET_TIMEOUT setting) to use temporary links, and finally the URL according the action which is performed (in this case the email activation).
Other point to consider is in each of this classes has template assigned and you can override it.
Now, in the views, specifically in UserViewSet and its actions perform_create(), perform_update() and resend_activation(), if the Djoser setting SEND_ACTIVATION_EMAIL is True, call to ActivationEmail to send an email to the user address.
def perform_create(self, serializer):
user = serializer.save()
signals.user_registered.send(
sender=self.__class__, user=user, request=self.request
)
context = {"user": user}
to = [get_user_email(user)]
if settings.SEND_ACTIVATION_EMAIL:
settings.EMAIL.activation(self.request, context).send(to)
...
The email is sent and when a user click the link, whether the token is still valid and uid match (djoser.UidAndTokenSerializer), the action activation() of the same View is executed. Change the user flag 'is_active' to True and it may sent another email to confirm the activation.
If you want code your own version, as you can see, you only have to create a random token, generate some uid to identify the user in the way that you prefer. Code a pair of views that send emails with templates that permit the activation.

How to override GET method depending on JWT payload in Django Rest Framework?

First of all, I hope the title of this question is clear enough. The following lines will make things clearer if it is not the case.
I have different users in my database. Some are part of the staff and some are just regular users.
In my API, I have a /users route. My idea is that when a staff member requests it (GET), he will see only the staff users and when a regular user will request it, he will only see the regular users.
Whether or not the user which makes the request is a member of the staff or not is an information which is stored in the JWT token.
Therefore, I used the following code:
class CustomUserList(generics.ListCreateAPIView):
serializer_class = CustomUserSerializer
def get_queryset(self):
token = self.request.META.get('HTTP_AUTHORIZATION')[7:] # [7:] to get rid of 'Bearer '
is_staff = jwt.decode(token, None, None)['is_staff']
print(is_staff)
queryset = CustomUser.objects.filter(is_staff=is_staff)
return queryset
This code works but is there a more direct / logical way to accomplish this ?
It feels a bit off storing what is essentially a server-side permission/setting on the client side (even though some people do use JWT for that). What if a user is promoted to/demoted from staff? You'd have to revoke all such client tokens? Also, what if you need to attach more conditions to these queries?
I think a slightly more flexible approach is to store some kind of a user id in the JWT token, then use a TokenAuthentication class with JWT to establish the identity of the user, attach it to something like request.user and then in your get_queryset method filter by request.user.is_staff. That way, you can attach any context/filters/permissions to the authenticated users on the server side and don't need to rely on the client to present explicit claim(s) to filter out the objects they can access. You'd end up with an extra call to the database to populate request.user, but you might end up needing that anyway.
Authenticating with a JWT token in the header retrieves the user so your view will have access to request.user and all of the user's attributes in every method after the dispatch() method (the first method that is run after .as_view() is triggered when sending a request to an endpoint).
If you're using djangorestframework-simplejwt, the DRF recommended JWT package, all you need to do is this:
class CustomUserList(generics.ListCreateAPIView):
queryset = CustomUser.objects.all()
serializer_class = CustomUserSerializer
def get_queryset(self):
queryset = super().get_queryset()
if self.request.user.is_staff:
queryset = queryset.filter(is_staff=True)
return queryset

How to logout user when he changes password from all browsers (Django-rest-auth, JWT)?

First of all, i am new with django-rest-framework so please excuse me if I'm wrong.
I'm working with django-rest-auth and django-restframework-jwt to authenticate users. I'm saving the jwt token in localStorage everytime the user logs in.
The problem That I'm facing now is that when I log in with same credentials in two browsers and then I change password in one of them, the other account still valid and user still can navigate and see all pages even though the password has changed.
I wanted to make his JWT token invalid when he changes password so that he will be automatically logged out. But I couldn't find a way to expire his token in official documentation of Django REST framework JWT
I tried to track the moment of changing password by generating manually a new JWT token for user, but this is not working (maybe because the existing token is still valid)
#receiver(signals.pre_save, sender=User)
def revoke_tokens(sender, instance, **kwargs):
existing_user = User.objects.get(pk=instance.pk)
if getattr(settings, 'REST_USE_JWT', False):
if instance.password != existing_user.password:
# If user has changed his password, generate manually a new token for him
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(instance)
payload['orig_iat'] = timegm(datetime.utcnow().utctimetuple())
instance.token = jwt_encode_handler(payload)
After reading some documenations and posts, it seems that this is not quite easy with only jwt since it's stateless, But could somebody point me the direction where to go?
Should I remove JWT authentication?
Is there a work around that can help me on this ?
Thanks a lot.
EDIT:
I found a comment in a similar post on SO by #Travis stating that
A common approach for invalidating tokens when a user changes their
password is to sign the token with a hash of their password. Thus if
the password changes, any previous tokens automatically fail to
verify. You can extend this to logout by including a last-logout-time
in the user's record and using a combination of the last-logout-time
and password hash to sign the token. This requires a DB lookup each
time you need to verify the token signature, but presumably you're
looking up the user anyway
I'm trying to implement that ..I will update my post if it worked.
Otherwise, I still open to suggestions.
After days of work, I ended up by overriding the JWT_PAYLOAD_HANDLER and adding the last digits of the user's hash of password in the payload of JWT token (since adding all the hash of password in the payload is not a good practice)
and then creating a custom middleware that intercepts all requests.
in every request I check from jwt token if the hash of the password matches the existing user's hash (if not that means that the user has changed his password)
if they are different then I raise an error and logout the user with old hash of password.
in config file :
'JWT_PAYLOAD_HANDLER': 'your.path.jwt.jwt_payload_handler',
and in the root stated in the config file :
def jwt_payload_handler(user):
username_field = get_username_field()
username = get_username(user)
payload = {
'user_id': user.pk,
'username': username,
'pwd': user.password[-10:],
'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA
}
if hasattr(user, 'email'):
payload['email'] = user.email
if isinstance(user.pk, uuid.UUID):
payload['user_id'] = str(user.pk)
payload[username_field] = username
return payload
and then this is the custom middleware :
from django.http.response import HttpResponseForbidden
from django.utils.deprecation import MiddlewareMixin
from rest_framework_jwt.utils import jwt_decode_handler
from config.settings.base import JWT_AUTH
from trp.users.models import User
class JWTAuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
jwt_user_pwd = self.get_jwt_user_pwd(request)
# check if last digits of password read from jwt token matches the hash of the current user in DB
if jwt_user_pwd is not None:
if jwt_user_pwd['pwd'] != jwt_user_pwd['user'].password[-10:]:
return HttpResponseForbidden()
#staticmethod
def get_jwt_user_pwd(request):
token = request.META.get('HTTP_AUTHORIZATION', None)
# Remove the prefix from token name so that decoding the token gives us correct credentials
token = str(token).replace(JWT_AUTH['JWT_AUTH_HEADER_PREFIX'] + ' ', '')
if token:
try:
payload = jwt_decode_handler(token)
authenticated_user = User.objects.get(id=payload['user_id'])
except Exception as e:
authenticated_user = None
payload = {}
if authenticated_user and payload:
return {'user': authenticated_user, 'pwd': payload.get('pwd')}
return None
To logout the user I have read the status code of the request 'in this case 403' from front end : (I'm using Angular in my case) and then logout the user
I hope it helps someone in the future .
Well,
It is all about token expiry time - If you keep this short (like 10-15 minutes) - you can no bother with invalidating it when a password or some permissions will change. Token will be invalidated always after some short period of time and a new one will be issued.
If you are using JWT as long living token (which is not good practice) - you will have problems.
Because actions like changing a password and invalidating other tokens (different session) (or force recreate) needs to be stored somewhere else (like some NoSQL store) - and checked for each session that some special action is required - and then you are losing the stateless advantage of JWT.

What is a safe way to allow REST APIs to be public?

In my website, people can see images given from REST APIs without login. I want to make some of my APIs not to require a user token. So, I had some research and I found the way below.
Since I need to use this way for production, I want to make sure if it is a safe way. The stores that will be given from the API have just store data such as name, urls, description, and images. Any of them are not related to any users.
Is it okay for me to use in production? Just so you know, I use Django REST Framework to serve data to frontend and use React js to show them in frontend side.
from rest_framework.decorators import authentication_classes, permission_classes
#authentication_classes([])
#permission_classes([])
class ListAllStores(APIView):
def get(self, request, format=None):
all_stores = Store.objects.all()
serializer = StoreSerializer(all_stores, many=True)
return Response(data=serializer.data)
You can try something link sending user type on header of each request.
And for the API without token send userType as anonymous or something like that
And for the API with token send userType as customer or something like that.
Create a method which will be called first thing from the each end points and validate the Request header.
If you want to make it more general you can map this end point and type of user as meta data setting, So next time if you have some '/image' end point which was allowed only to the customer user type but now you want it should be allowed to anon also so that can be easily done using that meta data file without changing the code.
You can be explicit by using AllowAny permission class, which will let public access to that endpoint.
from rest_framework import permissions, views
class ListAllStores(views.APIView):
permission_classes = (
permissions.AllowAny,
)
def get(self, request, format=None):
all_stores = Store.objects.all()
serializer = StoreSerializer(all_stores, many=True)
return Response(data=serializer.data)

django rest framework - adding to views.obtain_auth_token

I have implemented Token Authentication with django rest framework and I can post username and password to /api-token-auth/ and get the token.
url(r'^api-token-auth/', token_views.obtain_auth_token)
In addition to the token, I want to get the User object related to the returned token.
How can I override/add to this view and also return the actual User object?
You can find the relevant view here:
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/authtoken/views.py#L21
Assuming you've created some sort of User serializer already, you can basically take the user instance there and shove it into your UserSerializer. then add it to the response, something like the below.
...
user_serializer = UserSerializer(user)
return Response({'token': token.key, 'user': user_serializer.data})

Resources