User registration endpoint in Django Rest Framework - django

I'm using Django rest framework. I've written the following view to register a new user to the system.
#api_view(['POST'])
#csrf_exempt
#permission_classes((AllowAny, ))
def create_user(request):
email = request.DATA['email']
password = request.DATA['password']
try:
user = User.objects.get(email=email)
false = False
return HttpResponse(json.dumps({
'success': False,
'reason': 'User with this email already exists'
}), content_type='application/json')
except User.DoesNotExist:
user = User(email=email, username=email)
user.set_password(password)
user.save()
profile = UserProfile(user=user)
profile.save()
profile_serialized = UserProfileSerializer(profile)
token = Token(user=user)
token.save()
return HttpResponse(json.dumps({
'success': True,
'key': token.key,
'user_profile': profile_serialized.data
}), content_type='application/json')
Is there a better, slightly more secure way, of creating a user registration api in DRF that doesn't leave the endpoint so open to sql injection?

Forgive me to digress a little, but I can't help but wonder use could have gotten away with far less code, than you have if you had created a serializer and used a class-based view. Besides, if you had just created email as EmailField of serializer it would have automatically guaranteed the validation of email. Since you are using orm interface, risk of sql injection is much less than raw query in my opinion.
Sample Code:-
class UserList(CreateAPIView):
serializer_class = UserSerializer
class UserSerializer(ModelSerializer):
email = serializers.EmailField()
raw_password = serializers.CharField()
Something on these lines, Obviously I couldn't write entire code.

You could validate the email in your serializer using the validate-email-address module like this:
from validate_email_address import validate_email
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
def validate_email(self, value):
if not validate_email(value):
raise serializers.ValidationError("Not a valid email.")
return value
class Meta:
model = User
fields = ('email', 'password')
Also, you might consider a packaged auth/registration solution like Djoser.

Related

Django Rest Framework - Check Password to Validate Form

I'm trying to validate a form through DRF, but it would require the user to enter their password for confirmation. I can't seem to get it to work. Here is my current View and Serializer. Its for a 'change email' form, two fields required, the email and user password. It's for a seperate email model. The serializer:
class UpdateEmailAddressSerializer(serializers.ModelSerializer):
class Meta:
model = EmailAddress
fields = ('email',)
And the APIView:
class UpdateEmailAPI(APIView):
permission_classes = (IsAuthenticated,)
serializer_class = UpdateEmailAddressSerializer
def post(self, request, user, format=None):
user = User.objects.get(username=user)
serializer = UpdateEmailAddressSerializer(data=request.data, instance=user)
if serializer.is_valid():
## logic to check and send email
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I'm not sure where to place the password or what to do with it. Its from the User model itself. When I attempted to add password to the fields in the UpdateEmail serializer it ended up updating the User password with plain text and making that user object unable to use that password.
I just want to check the password of the user for confirmation of this form. Is there an obvious way to do this?
EDIT
When I attempt to bring 'password' into the serializer, an error tells "Field name password is not valid for model EmailAddress." So when I attempt to bring it in e.g.
password = serializers.CharField(required=True)
or try:
## UserPasswordSerializer
class UserPasswordSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'password',
)
## In UpdateEmailAddressSerializer
password = UserPasswordSerializer()
I get this error when submitting the form on DRF:
Got AttributeError when attempting to get a value for field
password on serializer UpdateEmailAddressSerializer. The
serializer field might be named incorrectly and not match any
attribute or key on the EmailAddress instance. Original exception
text was: 'EmailAddress' object has no attribute 'password'
So it seems to be telling me password isn't part of EmailAddress model which is correct. But I cant figure out how to simply check the password alongside the form post without making it part of EmailAddress.
I think you can try like this:
class UpdateEmailAddressSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = EmailAddress
fields = ('email', 'password',)
def create(self, validated_data):
validated_data.pop('password', None)
return super(UpdateEmailAddressSerializer, self).create(validated_data)
def update(self, instance, validated_data):
if instance.check_password(validated_data.get('password')):
instance.email = validated_data.get('email', instance.email)
# else throw validation error
return instance

Error about Django custom authentication and login?

I create a custom Authentication backends for my login system. Surely, the custom backends works when I try it in python shell. However, I got error when I run it in the server. The error says "The following fields do not exist in this model or are m2m fields: last_login". Do I need include the last_login field in customer model or Is there any other solution to solve the problem?
Here is my sample code:
In my models.py
class Customer(models.Model):
yes_or_no = ((True, 'Yes'),(False, 'No'))
male_or_female = ((True,'Male'),(False,'Female'))
name = models.CharField(max_length=100)
email = models.EmailField(max_length=100,blank = False, null = False)
password = models.CharField(max_length=100)
gender = models.BooleanField(default = True, choices = male_or_female)
birthday = models.DateField(default =None,blank = False, null = False)
created = models.DateTimeField(default=datetime.now, blank=True)
_is_active = models.BooleanField(default = False,db_column="is_active")
#property
def is_active(self):
return self._is_active
# how to call setter method, how to pass value ?
#is_active.setter
def is_active(self,value):
self._is_active = value
def __str__(self):
return self.name
In backends.py
from .models import Customer
from django.conf import settings
class CustomerAuthBackend(object):
def authenticate(self, name=None, password=None):
try:
user = Customer.objects.get(name=name)
if password == getattr(user,'password'):
# Authentication success by returning the user
user.is_active = True
return user
else:
# Authentication fails if None is returned
return None
except Customer.DoesNotExist:
return None
def get_user(self, user_id):
try:
return Customer.objects.get(pk=user_id)
except Customer.DoesNotExist:
return None
In views.py
#login_required(login_url='/dataInfo/login/')
def login_view(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
user = authenticate(name=username,password=password)
if user is not None:
if user.is_active:
login(request,user)
#redirect to user profile
print "suffcessful login!"
return HttpResponseRedirect('/dataInfo/userprofile')
else:
# return a disable account
return HttpResponse("User acount or password is incorrect")
else:
# Return an 'invalid login' error message.
print "Invalid login details: {0}, {1}".format(username, password)
# redirect to login page
return HttpResponseRedirect('/dataInfo/login')
else:
login_form = LoginForm()
return render_to_response('dataInfo/login.html', {'form': login_form}, context_instance=RequestContext(request))
In setting.py
AUTHENTICATION_BACKENDS = ('dataInfo.backends.CustomerAuthBackend', 'django.contrib.auth.backends.ModelBackend',)
This is happening because you are using django's login() function to log the user in.
Django's login function emits a signal named user_logged_in with the user instance you supplied as argument. See login() source.
And this signal is listened in django's contrib.auth.models. It tries to update a field named last_login assuming that the user instance you have supplied is a subclass of django's default AbstractUser model.
In order to fix this, you can do one of the following things:
Stop using the login() function shipped with django and create a custom one.
Disconnect the user_logged_in signal from update_last_login receiver. Read how.
Add a field named last_login to your model
Extend your model from django's base auth models. Read how
Thanks, I defined a custom login method as follows to get through this issue in my automated tests in which I by default keep the signals off.
Here's a working code example.
def login(client: Client, user: User) -> None:
"""
Disconnect the update_last_login signal and force_login as `user`
Ref: https://stackoverflow.com/questions/38156681/error-about-django-custom-authentication-and-login
Args:
client: Django Test client instance to be used to login
user: User object to be used to login
"""
user_logged_in.disconnect(receiver=update_last_login)
client.force_login(user=user)
user_logged_in.connect(receiver=update_last_login)
This in turn is used in tests as follows:
class TestSomething(TestCase):
"""
Scenarios to validate:
....
"""
#classmethod
#factory.django.mute_signals(signals.pre_save, signals.post_save)
def setUpTestData(cls):
"""
Helps keep tests execution time under control
"""
cls.client = Client()
cls.content_type = 'application/json'
def test_a_scenario(self):
"""
Scenario details...
"""
login(client=self.client, user=<User object>)
response = self.client.post(...)
...
Hope it helps.

How do I prevent the creation of a user if validation on the nested serializer fails?

I am creating a REST api for user registration, and I have a nested serializer where I store additional information about a user.
The User serializer asks for first_name, last_name, email, and password.
The nested serializer asks for agreed_terms_of_service
email, password, and agreed_terms_of_service are required.
But if a user keys in their email and password and DOES NOT check the agreed_terms_of_service box, it returns and error, but still creates a user with the email and password.
Then when the user goes to 'remedy the situation', the email address is already in use.
If I update instead of create, I feel like I would run into a situation where people are overwriting other users...
I am wondering how people handle this with django rest serializers and what is the best practice?
VIEWS.PY
def serialize(self, request):
if request.method =='POST':
data = json.loads(request.body)
#first validation
if data['password'] != data['password2']:
raise serializers.ValidationError({'msgType':'error','message':'Passwords do not match.'})
#move to serializer
else:
serializer = userSerializer(data = data)
data['username'] = data['email']
if serializer.is_valid(raise_exception=True):
serializer.save()
response = {'msgType':'success', 'message':'Your account has been created successfully.'}
elif serializer.errors:
raise serializers.ValidationError({'msgType':'error', 'message': serializer.errors})
return Response(response)
SERIALIZERS.PY
class nestedSerializer(serializers.ModelSerializer):
class Meta:
model = Nested
fields = ('agreed_terms_of_service')
def validate(self, data):
return data
class userSerializer(serializers.ModelSerializer):
nested = nestedSerializer()
class Meta:
model = User
fields = ('pk','email', 'password', 'username','first_name','last_name','nested')
def validate(self, data):
email = data['email']
try:
User.objects.get(email = email)
except User.DoesNotExist:
return data
else:
raise serializers.ValidationError({'msgType':'error', 'message':'A user with this email address already exists.'})
return data
def create(self, validated_data):
nested_data = validated_data.pop('extend')
email = validated_data['email']
user = User.objects.create(**validated_data)
user.username = user.id
user.set_password(validated_data['password'])
user.save()
nested = Nested.objects.create(user=user, **nested_data)
return user
Models.py
class Nested(models.Model):
user = models.OneToOneField(User)
personalid = models.CharField(max_length=255)
agreed_terms_of_service = models.BooleanField()
city = models.CharField(max_length=255, blank=True, null=True)
Thank you for your help in advance. It is much appreciated.
First, I'd change your current validate() function to validate_email() (because all you're doing is validating that the email is not already in use). You should use validate() if you want access to multiple fields in your function. See the documentation here to read more about when you should use field-level validation and object-level validation: http://www.django-rest-framework.org/api-guide/serializers/#validation
Second, in your view, you do:
if data['password'] != data['password2']:
raise serializers.ValidationError({'msgType':'error','message':'Passwords do not match.'})
If you're verifying that "password" and "confirm password" field match, I'd do that check in the validate() function of your serializer (since you'll be accessing both the 'password' and the 'password2' field.
Third, in your create method, I'd use User.objects.create_user to create a user (create_user will handle the hashing of the password, etc. That way, you don't need to explicitly do user.set_password(validated_data['password'])). See the answer here for more information: How to create a user in Django?
Lastly, to address the main issue. Your "agreed_terms_of_service" is a Boolean field, which means it accepts both True and False. What I'd try is this:
class nestedSerializer(serializers.ModelSerializer):
class Meta:
model = Nested
fields = ('agreed_terms_of_service')
def validate_agreed_terms_of_service(self, data):
print(data) # to verify if data is even a boolean
if data == True or data == 'True':
return data
raise serializers.ValidationError({'msgType':'error', 'message':'Please accept the terms and conditions.'})
and in your create function for your userSerializer, add a print statement at the beginning to see if create is being executed before the "agreed_terms_of_service" validation.
def create(self, validated_data):
print("Creating the object before validating the nested field.")
# I'd be surprised if DRF executes a create function before
# even validating it's nested fields.
# rest of the create code goes here
When you add the statements above, what does it print for "data" and does it print "data" before "creating the object"?

Changing serializer fields on the fly

For example I have the following serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'userid',
'password'
)
But I don't want to output password on GET (there are other fields in my real example of course). How can I do that without writing other serializer? Change the field list on the fly. Is there any way to do that?
You appear to be looking for a write-only field. So the field will be required on creation, but it won't be displayed back to the user at all (the opposite of a read-only field). Luckily, Django REST Framework now supports write-only fields with the write_only attribute.
In Django REST Framework 3.0, you just need to add the extra argument to the extra_kwargs meta option.
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'userid',
'password'
)
extra_kwargs = {
'password': {
'write_only': True,
},
}
Because the password should be hashed (you are using Django's user, right?), you are going to need to also hash the password as it is coming in. This should be done on your view, most likely by overriding the perform_create and perform_update methods.
from django.contrib.auth.hashers import make_password
class UserViewSet(viewsets.ViewSet):
def perform_create(self, serializer):
password = make_password(self.request.data['password'])
serializer.save(password=password)
def perform_update(self, serializer):
password = make_password(self.request.data['password'])
serializer.save(password=password)
In Django REST Framework 2.x, you need to completely redefine the password field on the serializer.
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = (
'userid',
'password'
)
In order to hash the password ahead of time in Django REST Framework 2.x, you need to override pre_save.
from django.contrib.auth.hashers import make_password
class UserViewSet(viewsets.ViewSet):
def pre_save(self, obj, created=False):
obj.password = make_password(obj.password)
super(UserViewSet, self).pre_save(obj, created=created)
This will solve the common issue with the other answers, which is that the same serializer that is used for creating/updating the user will also be used to return the updated user object as the response. This means that the password will still be returned in the response, even though you only wanted it to be write-only. The additional problem with this is that the password may or may not be hashed in the response, which is something you really don't want to do.
this should be what you need. I used a function view but you can use class View or ViewSet (override get_serializer_class) if you prefer.
Note that serializer_factory also accept exclude= but, for security reason, I prefer use fields=
serializer_factory create a Serializer class on the fly using an existing Serializer as base (same as django modelform_factory)
==============
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'userid',
'password'
)
#api_view(['GET', 'POST'])
def user_list(request):
User = get_user_model()
if request.method == 'GET':
fields=['userid']
elif request.method == 'POST':
fields = None
serializer = serializer_factory(User, UserSerializer, fields=fields)
return Response(serializer.data)
def serializer_factory(model, base=HyperlinkedModelSerializer,
fields=None, exclude=None):
attrs = {'model': model}
if fields is not None:
attrs['fields'] = fields
if exclude is not None:
attrs['exclude'] = exclude
parent = (object,)
if hasattr(base, 'Meta'):
parent = (base.Meta, object)
Meta = type(str('Meta'), parent, attrs)
if model:
class_name = model.__name__ + 'Serializer'
else:
class_name = 'Serializer'
return type(base)(class_name, (base,), {'Meta': Meta, })
Just one more thing to #Kevin Brown's solution.
Since partial update will also execute perform_update, it would be better to add extra code as following.
def perform_update(self, serializer):
if 'password' in self.request.data:
password = make_password(self.request.data['password'])
serializer.save(password=password)
else:
serializer.save()
As far as i can tell from the docs, the fastest way would be to simply have 2 serializers becalled conditionally from your view.
Also, the docs show this other alternative, but it's a little too meta:
http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
it involves creating smart initializer methods, gives an example. i'd just use 2 serializers, if i'd know those changes are the only ones i'll make. otherwise, check the example

Adding the auth token to the UserSerializer

How would I add the auth token to the userSeralizer?
This is my serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username')
And then in my views the url:
#api_view(['POST', 'DELETE'])
def create_user(request):
"""
API endpoint to register a new user
"""
model = User
serializer_class = UserSerializer
username, password = request.POST['username'], request.POST['password']
try:
user = User.objects.create_user(username, username, password)
except IntegrityError:
user = User.objects.get(username=username, email=username)
# the users token, we will send this to him now.
token = Token.objects.get(user=user)
if request.method == "POST":
serializer = UserSerializer(user)
return Response(data)
I think it would be nice to have the token in the serializer, or not?
From a security standpoint, auth tokens should not be passed around in the serializer. If your User view can be seen by anyone, then anyone could to impersonate any user without much trouble.
Tokens are meant to be returned only after successful login, not when an user is created. This is why most sites require Users to sign in just after the account was created.
But for the sake of the question, there are several ways to add items to serializers.
First, is a little hacky but doesn't require custom models
# Not adding context would raise a DeprecationWarning in the console
serializer = UserSerializer(user, context={'request': request})
data = serializer.data
data['token'] = token
return Response(data)
Last but not least, is a bit more elegant but requires a custom User class. However you could use it in your app models.
# in models.py inside your User model
def get_my_token(self):
return Token.objects.get(user=user)
my_token = property(get_my_token)
and then in the serializer class add the field with the token (remember to add it to the fields attribute in your meta class)
class UserSerializer(serializers.ModelSerializer):
token = serializers.Field(source='my_token')
class Meta:
model = User
fields = ('id', 'username', 'token')