Reputation: 821
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
Upvotes: 53
Views: 60238
Reputation: 1810
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)
Upvotes: 37
Reputation: 175
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"})
Upvotes: 1
Reputation: 729
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
Upvotes: 0
Reputation: 31
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]
Upvotes: 3
Reputation: 1170
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)
Upvotes: 96
Reputation: 586
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})
Upvotes: 1
Reputation: 1855
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}
Upvotes: 6
Reputation: 1763
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)
Upvotes: 12
Reputation: 423
@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)
Upvotes: 42