18

I am using the Django REST Framework (DRF) to create an endpoint with which I can register new users. However, when I hit the creation endpoint with a POST, the new user is saved via a serializer, but the password is saved in cleartext in the database. The code for my serializer is as follows:

from django.contrib.auth import get_user_model
from rest_framework import serializers

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = get_user_model()
        fields = ['password', 'username', 'first_name', 'last_name', 'email']
        read_only_fields = ['is_staff', 'is_superuser']
        write_only_fields = ['password']

Please note that I am using the default User model from the Django auth package, and that I am very new to working with DRF! Additionally, I have found this question which provides a solution, but this appears to require two database interactions -- I do not believe that this is efficient, but that might be an incorrect assumption on my part.

Community
  • 1
  • 1
nmagerko
  • 6,586
  • 12
  • 46
  • 71
  • Here is another solution which overriding `perform_create` and `perform_update` : http://stackoverflow.com/questions/27468552/changing-serializer-fields-on-the-fly/#answer-27471503 – Chemical Programmer Dec 09 '15 at 13:07

4 Answers4

38

The issue is DRF will simply set the field values onto the model. Therefore, the password is set on the password field, and saved in the database. But to properly set a password, you need to call the set_password() method, that will do the hashing.

There are several ways to do this, but the best way on rest framework v3 is to override the update() and create() methods on your Serializer.

class UserSerializer(serializers.ModelSerializer):
    # <Your other UserSerializer stuff here>

    def create(self, validated_data):
        password = validated_data.pop('password', None)
        instance = self.Meta.model(**validated_data)
        if password is not None:
            instance.set_password(password)
        instance.save()
        return instance

    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

Two things here:

  1. we user self.Meta.model, so if the model is changed on the serializer, it still works (as long as it has a set_password method of course).
  2. we iterate on validated_data items and not the fields, to account for optionally excludeed fields.

Also, this version of create does not save M2M relations. Not needed in your example, but it could be added if required. You would need to pop those from the dict, save the model and set them afterwards.

FWIW, I thereby make all python code in this answer public domain worldwide. It is distributed without any warranty.

spectras
  • 13,105
  • 2
  • 31
  • 53
  • Great answer, but I'm a bit new to this, so I'll have to ask what you mean by not saving model to model relations. If I'm using a `ModelSerializer`, won't this functionality come automatically? – nmagerko Dec 21 '14 at 03:19
  • Specifically, an answer here (http://stackoverflow.com/a/13564519/996249, note the author) says that I can just modify `serializer.object` – nmagerko Dec 21 '14 at 03:27
  • 1
    For model2model relations, what I mean is if you are saving other models connected to the user, using a nested serializer, the sample I provided will only save the user, not the other objects. As for your link, it is about REST Framework v2. Version 3 changed the API regarding model saving. – spectras Dec 21 '14 at 03:30
  • 1
    In case someone new runs into the same problem, the original answer does not state where do these methods need to be added. I was trying to add these methods to Views but actually it should be added to serializer for the model. May be it is evident from the method signature but for a newbie, it may not be so obvious. – Divick Mar 23 '15 at 05:16
  • 1
    @DivKis01> Edited to make it clear the methods live on the Serializer. Thanks. – spectras Mar 23 '15 at 16:20
  • @spectras I did test your answer, and when I create an user via DRF, I cannot login with this user which I've created. It's strange. Do you know what happened? – bgarcial Jan 20 '17 at 03:03
  • 1
    @bgarcial> I don't, but you did the right thing opening [another question](http://stackoverflow.com/questions/41756139/creating-users-with-django-rest-framework-not-authenticate) and I'm glad someone helped you with your issue. – spectras Jan 22 '17 at 22:17
  • @spectras it's true. Thanks to you for the support intention and your answer here. This was very useful for me :D – bgarcial Jan 24 '17 at 18:52
  • For some reason if I use `instance = self.Meta.model(**validated_data)` it blows up and says my user "needs to have a value for field "id" before this many-to-many relationship can be used." But if I change remove **validated_data and explicitly name the fields to update, it works. Anyone know what that is? – Drew S Oct 17 '17 at 19:41
  • @DrewS> probably your validated data has some extra information, and one of the items is a foreign relation. You may either remove it before instantiating the model or explicitly specify which fields to use as you did. – spectras Oct 17 '17 at 22:32
3

This worked for me.

class UserSerializer(serializers.ModelSerializer):
    def create(self, *args, **kwargs):
        user = super().create(*args, **kwargs)
        p = user.password
        user.set_password(p)
        user.save()
        return user

    def update(self, *args, **kwargs):
        user = super().update(*args, **kwargs)
        p = user.password
        user.set_password(p)
        user.save()
        return user

    class Meta:
        model = get_user_model()
        fields = "__all__" 
suhailvs
  • 20,182
  • 14
  • 100
  • 98
2

just override the create and update methods of the serializer:

   def create(self, validated_data):
        user = get_user_model(**validated_data)
        user.set_password(validated_data['password'])
        user.save()
        return user

    def update(self, instance, validated_data):
        for f in UserSerializer.Meta.fields + UserSerializer.Meta.write_only_fields:
            set_attr(instance, f, validated_data[f])
        instance.set_password(validated_data['password'])
        instance.save()
        return instance
DRC
  • 4,898
  • 2
  • 21
  • 35
  • Is there a significant difference between your approach and the approach of the answer below? They look somewhat similar to me – nmagerko Dec 21 '14 at 03:25
  • @nmagerko no the approach is exactly the same, I don't check if password could be None, I was preferring brevity to outline the method and leave you the actual implementation, if that cares to you then please change that ! – DRC Dec 21 '14 at 03:30
  • Unfortunately, I must accept the other answer; using the `get_user_model` will not always work here, since some keywords provided in the validated data are extraneous, and will throw an error :( – nmagerko Dec 21 '14 at 03:40
  • @nmagerko I also prefer that approach! – DRC Dec 21 '14 at 03:42
0

Here is an alternative to accepted answer.

class CreateUserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('email', 'username', 'password')
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User.objects.create_user(
            email=validated_data['email'],
            username=validated_data['username'],
            password=validated_data['password'],
        )
        user.save()
        return user

create_user function is defined in UserManager and it uses set_password(), we don't need to use it explicitly. I have found many answers and articles which suggest to use set_password but after trying many things I figured the above and it works with CustomUserManager too. Suppose phone number and password is required to register a user. So our CustomUserManager will look something like this and CreateUserSerializer will handle this too with no changes.


class CustomUserManager(BaseUserManager):

    def create_user(self, phone_number, password):
        if not phone_number:
            raise ValueError('Phone Number must be set')

        user = self.model(phone_number=phone_number)
        user.set_password(password)
        user.save(using=self._db)
        return user
neferpitou
  • 415
  • 7
  • 14