How to update user password in Django Rest Framework?

Question:

I want to ask that following code provides updating password but I want to update password after current password confirmation process. So what should I add for it? Thank you.

class UserPasswordSerializer(ModelSerializer):

    class Meta:
        model = User
        fields = [
            'password'
        ]

        extra_kwargs = {
            "password": {"write_only": True},
        }

    def update(self, instance, validated_data):
        for attr, value in validated_data.items():
            if attr == 'password':
                instance.set_password(value)
            else:
                setattr(instance, attr, value)
        instance.save()
        return instance
Asked By: bysucpmeti

||

Answers:

I believe that using a modelserializer might be an overkill. This simple serializer & view should work.

Serializers.py

from rest_framework import serializers
from django.contrib.auth.models import User

class ChangePasswordSerializer(serializers.Serializer):
    model = User

    """
    Serializer for password change endpoint.
    """
    old_password = serializers.CharField(required=True)
    new_password = serializers.CharField(required=True)

Views.py

from rest_framework import status
from rest_framework import generics
from rest_framework.response import Response
from django.contrib.auth.models import User
from . import serializers
from rest_framework.permissions import IsAuthenticated   

class ChangePasswordView(UpdateAPIView):
        """
        An endpoint for changing password.
        """
        serializer_class = ChangePasswordSerializer
        model = User
        permission_classes = (IsAuthenticated,)

        def get_object(self, queryset=None):
            obj = self.request.user
            return obj

        def update(self, request, *args, **kwargs):
            self.object = self.get_object()
            serializer = self.get_serializer(data=request.data)

            if serializer.is_valid():
                # Check old password
                if not self.object.check_password(serializer.data.get("old_password")):
                    return Response({"old_password": ["Wrong password."]}, status=status.HTTP_400_BAD_REQUEST)
                # set_password also hashes the password that the user will get
                self.object.set_password(serializer.data.get("new_password"))
                self.object.save()
                response = {
                    'status': 'success',
                    'code': status.HTTP_200_OK,
                    'message': 'Password updated successfully',
                    'data': []
                }

                return Response(response)

            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Answered By: Yiğit Güler

@Yiğit Güler give a good answer, thanks, but it could be better in some minor points.

As long you don’t really works with UpdateModelMixin, but directly with the request user instance, you don’t need to use a UpdateAPIView. A simple APIView is enough.

Also, when the password is changed, you can return a status.HTTP_204_NO_CONTENT instead of a 200 with some random content.

By the way, don’t forgot to validate your new password before save. It’s too bad if you allow “password” at update while you don’t at create.

So I use the following code in my project:

from django.contrib.auth.password_validation import validate_password

class ChangePasswordSerializer(serializers.Serializer):
    """
    Serializer for password change endpoint.
    """
    old_password = serializers.CharField(required=True)
    new_password = serializers.CharField(required=True)

    def validate_new_password(self, value):
        validate_password(value)
        return value

And for the view:

class UpdatePassword(APIView):
    """
    An endpoint for changing password.
    """
    permission_classes = (permissions.IsAuthenticated, )

    def get_object(self, queryset=None):
        return self.request.user

    def put(self, request, *args, **kwargs):
        self.object = self.get_object()
        serializer = ChangePasswordSerializer(data=request.data)

        if serializer.is_valid():
            # Check old password
            old_password = serializer.data.get("old_password")
            if not self.object.check_password(old_password):
                return Response({"old_password": ["Wrong password."]}, 
                                status=status.HTTP_400_BAD_REQUEST)
            # set_password also hashes the password that the user will get
            self.object.set_password(serializer.data.get("new_password"))
            self.object.save()
            return Response(status=status.HTTP_204_NO_CONTENT)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Answered By: tominardi

After you save the user, you might want to make sure that the user stays logged in (after django==1.7 an user automatically is logged out on password change):

from django.contrib.auth import update_session_auth_hash

# make sure the user stays logged in
update_session_auth_hash(request, self.object)

I think the easiest (when I say easiest, I mean shortest possible and cleaner) solution would be something like:

View class

class APIChangePasswordView(UpdateAPIView):
    serializer_class = UserPasswordChangeSerializer
    model = get_user_model() # your user model
    permission_classes = (IsAuthenticated,)

    def get_object(self, queryset=None):
        return self.request.user

Serializer class

from rest_framework import serializers
from rest_framework.serializers import Serializer


class UserPasswordChangeSerializer(Serializer):
    old_password = serializers.CharField(required=True, max_length=30)
    password = serializers.CharField(required=True, max_length=30)
    confirmed_password = serializers.CharField(required=True, max_length=30)

    def validate(self, data):
        # add here additional check for password strength if needed
        if not self.context['request'].user.check_password(data.get('old_password')):
            raise serializers.ValidationError({'old_password': 'Wrong password.'})

        if data.get('confirmed_password') != data.get('password'):
            raise serializers.ValidationError({'password': 'Password must be confirmed correctly.'})

        return data

    def update(self, instance, validated_data):
        instance.set_password(validated_data['password'])
        instance.save()
        return instance

    def create(self, validated_data):
        pass

    @property
    def data(self):
        # just return success dictionary. you can change this to your need, but i dont think output should be user data after password change
        return {'Success': True}
Answered By: Igor

serializer.py

class UserSer(serializers.ModelSerializers):
      class meta:
          model=UserModel
          fields = '__all__'

views.py

class UserView(UpdateAPIView):
    serializer_class = serializers.UserSer
    queryset = models.User.objects.all()

    def get_object(self,pk):
        try:
            return models.User.objects.get(pk=pk)
        except Exception as e:
            return Response({'message':str(e)})

    def put(self,request,pk,format=None):
        user = self.get_object(pk) 
        serializer = self.serializer_class(user,data=request.data)

        if serializer.is_valid():            
            serializer.save()
            user.set_password(serializer.data.get('password'))
            user.save()
            return Response(serializer.data)    
        return Response({'message':True})
Answered By: Anar Ali

I dont’ think the validation should be done by the view as @Yiğit Güler proposes. Here is my solution:

serializers.py

from django.contrib.auth import password_validation
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers

class ChangePasswordSerializer(serializers.Serializer):
    old_password = serializers.CharField(max_length=128, write_only=True, required=True)
    new_password1 = serializers.CharField(max_length=128, write_only=True, required=True)
    new_password2 = serializers.CharField(max_length=128, write_only=True, required=True)

    def validate_old_password(self, value):
        user = self.context['request'].user
        if not user.check_password(value):
            raise serializers.ValidationError(
                _('Your old password was entered incorrectly. Please enter it again.')
            )
        return value

    def validate(self, data):
        if data['new_password1'] != data['new_password2']:
            raise serializers.ValidationError({'new_password2': _("The two password fields didn't match.")})
        password_validation.validate_password(data['new_password1'], self.context['request'].user)
        return data

    def save(self, **kwargs):
        password = self.validated_data['new_password1']
        user = self.context['request'].user
        user.set_password(password)
        user.save()
        return user

views.py

from rest_framework import status
from rest_framework.generics import UpdateAPIView
from rest_framework.authtoken.models import Token

class ChangePasswordView(UpdateAPIView):
    serializer_class = ChangePasswordSerializer

    def update(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        # if using drf authtoken, create a new token 
        if hasattr(user, 'auth_token'):
            user.auth_token.delete()
        token, created = Token.objects.get_or_create(user=user)
        # return new token
        return Response({'token': token.key}, status=status.HTTP_200_OK)

Answered By: p14z

So I decided to override the update function within ModelSerializer. Then get the password of the User instance. Afterwards run the necessary comparisons of making sure old password is the same as the one currently on the user instance via the check_password function and making sure new password and confirm password slot values are the same then proceed to set the new password if true and save the instance and return it.

serializers.py

   class ChangePasswordSerializer(ModelSerializer):
    confirm_password = CharField(write_only=True)
    new_password = CharField(write_only=True)
    old_password = CharField(write_only=True)

    class Meta:
        model = User
        fields = ['id', 'username', 'password', 'old_password', 'new_password','confirm_password']



    def update(self, instance, validated_data):

        instance.password = validated_data.get('password', instance.password)

        if not validated_data['new_password']:
              raise serializers.ValidationError({'new_password': 'not found'})

        if not validated_data['old_password']:
              raise serializers.ValidationError({'old_password': 'not found'})

        if not instance.check_password(validated_data['old_password']):
              raise serializers.ValidationError({'old_password': 'wrong password'})

        if validated_data['new_password'] != validated_data['confirm_password']:
            raise serializers.ValidationError({'passwords': 'passwords do not match'})

        if validated_data['new_password'] == validated_data['confirm_password'] and instance.check_password(validated_data['old_password']):
            # instance.password = validated_data['new_password'] 
            print(instance.password)
            instance.set_password(validated_data['new_password'])
            print(instance.password)
            instance.save()
            return instance
        return instance

views.py

    class ChangePasswordView(RetrieveUpdateAPIView):
        queryset= User.objects.all()
        serializer_class = ChangePasswordSerializer
        permission_classes = [IsAuthenticated]
Answered By: danielrosheuvel

I want to add another option, in case you have a ModelViewSet. This way you’d probably want to use an @action for the password updating, this way you can still handle every aspect of the user model using the ModelViewSet and still customize the behavior and serializer utilized on this action, and I would also add a custom permission to verify the user is trying to update it’s own information.

permissions.py:

from rest_framework import exceptions
from rest_framework.permissions import BasePermission, SAFE_METHODS
from django.utils.translation import gettext_lazy as _
from users.models import GeneralUser

class IsSelf(BasePermission):
    def has_object_permission(self, request, view, obj):
        if isinstance(obj, GeneralUser):
            return request.user == obj
        raise exceptions.PermissionDenied(detail=_("Received object of wrong instance"), code=403)

*I’m using my custom user model classGeneralUser

views.py:

from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser
from rest_framework.response import Response
from rest_framework import viewsets
from django.utils.translation import gettext_lazy as _
from users.api.serializers import UserSerializer, UserPwdChangeSerializer
from users.api.permissions import IsSelf

class UserViewSet(viewsets.ModelViewSet):
    __doc__ = _(
        """
        <Your Doc string>
        """
    )
    permission_classes = (IsAuthenticated, IsSelf)
    serializer_class = UserSerializer

    def get_queryset(self):
        return GeneralUser.objects.filter(pk=self.request.user.pk)


    def get_permissions(self):
        if self.action == 'create':
            permission_classes = [AllowAny]
        else:
            permission_classes = [IsAuthenticated]
        return [permission() for permission in permission_classes]

    # ....
    # Your other actions or configurations
    # ....

    @action(detail=True, methods=["put"])
    def upassword(self, request, pk=None):
        user = GeneralUser.objects.get(pk=pk)
        self.check_object_permissions(request, user)
        ser = UserPwdChangeSerializer(user, data=request.data, many=False, context={
            "user":request.user
        })
        ser.is_valid(raise_exception=True)
        user = ser.save()
        return Response(ser.data, status=status.HTTP_200_OK)

serializers.py:

from django.utils.translation import gettext_lazy as _
from django.contrib.auth.hashers import make_password
from django.core import exceptions
from django.contrib.auth.password_validation import validate_password as v_passwords
from rest_framework import serializers

from users.models import GeneralUser

class UserSerializer(serializers.ModelSerializer):
    __doc__ = _(
        """
        Serializer for User model
        """
    )

    class Meta:
        model = GeneralUser
        fields = '__all__'
        read_only_fields = ["last_login", "date_joined"]
        extra_kwargs = {'password': {'write_only': True}}


    def validate_password(self, value: str) -> str:
        try:
            v_passwords(value, GeneralUser)
            return make_password(value)
        except exceptions.ValidationError as e:
            raise serializers.ValidationError(e.messages)


class UserPwdChangeSerializer(serializers.Serializer):
    __doc__ = _(
        """
        Serializer for user model password change
        """
    )

    old_password = serializers.CharField(max_length=128, write_only=True, required=True)
    new_password1 = serializers.CharField(max_length=128, write_only=True, required=True)
    new_password2 = serializers.CharField(max_length=128, write_only=True, required=True)

    def validate_old_password(self, value):
        user = self.context['user']
        if not user.check_password(value):
            raise serializers.ValidationError(
                _('Your old password was entered incorrectly. Please enter it again.')
            )
        return value

    def validate(self, data):
        if data['new_password1'] != data['new_password2']:
            raise serializers.ValidationError({'new_password2': _("The two password fields didn't match.")})
        v_passwords(data['new_password1'], self.context['user'])
        return data

    def save(self, **kwargs):
        password = self.validated_data['new_password1']
        user = self.context['user']
        user.set_password(password)
        user.save()
        return user

I used @Pedro’s answer to configure the UserPwdChangeSerializer


With this implementation you’ll have a fully functional ModelViewSet for all fields updating and user creation as well as an action for password updating, in which you’ll be able to use old password and validate that the new password was inputted correctly twice.

The custom password change will be created inside the url path you use for your users which might be something like:

api/users/<user_pk>/upassword

Answered By: reojased

EDIT: Use capcha or something similar to escape from brute forces…


I did it with my own hacky way!
Might not the best way, but I found it better to understand,,,

**
Feel free to ask if anything seems to be a bouncer and I always encourage questions and feed backs…
**

I created a model for it.

class PasswordReset(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    key = models.CharField(max_length=100)
    timestamp = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

Added urls like these…

urlpatterns = [
    path("request/", password_reset_request),
    path("confirm/", password_reset_confirm),
]

And here we have our views…

@api_view(["POST"])
@permission_classes([AllowAny])
def password_reset_request(request):
    # checking username
    queryset = User.objects.filter(username=request.POST.get("username"))
    if queryset.exists():
        user = queryset.first()
    else:
        return Response({"error": "User does not exists!"})

    # Checking for password reset model
    queryset = PasswordReset.objects.filter(user=user)
    if queryset.exists():
        password_reset = PasswordReset.first()
        # checking for last password reset
        if password_reset.timestamp < timezone.now() - timedelta(days=1):
            # password is not recently updated
            password_reset.delete()
            password_reset = PasswordReset(
                user=user,
                key="".join(
                    [choice("!@$_-qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890") for i in range(99)]
                ),
            )
            password_reset.save()

            # send email here
            subject = "Password reset request"
            message = """To reset your password, go to localhost:8000/password_reset/{}""".format(password_reset.key)
            from_email = "[email protected]"
            recipient_list = [user.email]
            auth_user = "[email protected]"
            auth_password = "mechanicsareawesomeagain"
            send_mail(subject, message, from_email, recipient_list, auth_user=auth_user, auth_password=auth_password)

        else:
            # recent password updated
            return Response({"error": "Your password was updated recently, wait before updating it again."})


@api_view(["POST"])
@permission_classes([AllowAny])
def password_reset_confirm(request):
    # checking key
    queryset = PasswordReset.objects.filter(key=request.POST.get("key"))
    if queryset.exists():
        password_reset = queryset.first()

        if password_reset.timestamp < timezone.now() - timedelta(minutes=30):
            # expired
            return Response({"error": "Password reset key is expired! Try fresh after some hours."})

        else:
            # valid
            password = request.POST.get("password", "")
            if password == "":
                # valid key and waiting for password
                return Response({"success": "Set a new password"})

            else:
                # seting up the password
                user = password_reset.user
                user.set_password(password)
                user.save()
                return Response({"success": "Password updated successfully."})

    else:
        # invalid key
        return Response({"error": "Invalid key"})
Answered By: AgentNirmites