integrate django password validators with django rest framework validate_password
Asked Answered
U

5

33

I'm trying to integrate django validators 1.9 with django rest framework serializers. But the serialized 'user' (of django rest framework) is not compatible with the django validators.

Here is the serializers.py

import django.contrib.auth.password_validation as validators
from rest_framework import serializers

    class RegisterUserSerializer(serializers.ModelSerializer):

        password = serializers.CharField(style={'input_type': 'password'}, write_only=True)

        class Meta:
            model = User
            fields = ('id', 'username', 'email, 'password')

        def validate_password(self, data):
            validators.validate_password(password=data, user=User)
            return data

        def create(self, validated_data):
            user = User.objects.create_user(**validated_data)
            user.is_active = False
            user.save()
            return user

I managed to get MinimumLengthValidator and NumericPasswordValidator correct because both function validate don't use 'user' in validating. Source code is here

Excerpt from django source code:

def validate(self, password, user=None):
        if password.isdigit():
            raise ValidationError(
                _("This password is entirely numeric."),
                code='password_entirely_numeric',
            )

For other validators like UserAttributeSimilarityValidator, the function uses another one argument 'user' in validating ('user' is django User model, if I'm not wrong)

Excerpt from django source code:

 def validate(self, password, user=None):
        if not user:
            return

        for attribute_name in self.user_attributes:
            value = getattr(user, attribute_name, None)

How can I change serialized User into what django validators(UserAttributeSimilarityValidator) can see

Excerpt from django source code:

def validate(self, password, user=None):
        if not user:
            return

        for attribute_name in self.user_attributes:
            value = getattr(user, attribute_name, None)
            if not value or not isinstance(value, string_types):
                continue

Edit

Django Rest Framework can get all of Django's built-in password validation (but it's like a hack). Here's a problem:

The validationError is like this

[ValidationError(['This password is too short. It must contain at least 8 characters.']), ValidationError(['This password is entirely numeric.'])]

The validation doesn't contain a field. Django rest framework see it as

{
    "non_field_errors": [
        "This password is too short. It must contain at least 8 characters.",
        "This password is entirely numeric."
    ]
}

How can I inject a field at raise ValidationError

Unquestionable answered 4/4, 2016 at 23:51 Comment(0)
R
61

Like you mentioned, when you validate the password in validate_password method using UserAttributeSimilarityValidator validator, you don't have the user object.

What I suggest that instead of doing field-level validation, you shall perform object-level validation by implementing validate method on the serializer:

import sys
from django.core import exceptions
import django.contrib.auth.password_validation as validators

class RegisterUserSerializer(serializers.ModelSerializer):

     # rest of the code

     def validate(self, data):
         # here data has all the fields which have validated values
         # so we can create a User instance out of it
         user = User(**data)
         
         # get the password from the data
         password = data.get('password')
         
         errors = dict() 
         try:
             # validate the password and catch the exception
             validators.validate_password(password=password, user=user)
         
         # the exception raised here is different than serializers.ValidationError
         except exceptions.ValidationError as e:
             errors['password'] = list(e.messages)
         
         if errors:
             raise serializers.ValidationError(errors)
          
         return super(RegisterUserSerializer, self).validate(data)
Redraft answered 5/4, 2016 at 6:54 Comment(5)
There are a few typo in your code. By the way, your solution works fine on validating but django rest framework can't get the field name hence the non_field_errors ---> { "non_field_errors": [ "The password is too similar to the email.", "This password is too short. It must contain at least 8 characters." ] }Unquestionable
I haven't actually tested the code so there might be some typos here and there but that shouldn't stop you from using and testing this code properly. About the NON_FIELD_ERRORS I updated my answer and now the raised errors should contain password field name as key for those errors.Redraft
Bear in mind that **data may not include all required fields to create the user object when doing partial (PATCH) updates.Marker
This answer worked for me straight away even giving the exact 400 Bad Request response I want (the only typo is user=User). But where is it documented that raise serializers.ValidationError in validate will result in a 400 Bad Request response? django_rest_framework is a bit mysterious for me at the moment as I can't find the docs for this behaviour. Any advice appreciated. I would like to understand better.Ectoparasite
I had issues with associations not working when using User(**data), but using self.instance instead fixed that. This worked for me otherwise.Demurrer
M
18

You can access the user object through self.instance on the serializer object, even when doing field-level validation. Something like this should work:

 from django.contrib.auth import password_validation

 def validate_password(self, value):
    password_validation.validate_password(value, self.instance)
    return value
Marker answered 1/1, 2017 at 21:41 Comment(1)
This will only be available if the user has been created, which will not be the case when registering a new user and validating the password pre-creation.Forefoot
S
11

Use Serializers! Have a validate_fieldname method!

class UserSerializer(serializers.ModelSerializer):

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

    def validate_password(self, value):
        try:
            validate_password(value)
        except ValidationError as exc:
            raise serializers.ValidationError(str(exc))
        return value

    def create(self, validated_data):
        user = super().create(validated_data)
        user.set_password(validated_data['password'])

        user.is_active = False
        user.save()
        return user

    def update(self, instance, validated_data):
        user = super().update(instance, validated_data)
        if 'password' in validated_data:
            user.set_password(validated_data['password'])
            user.save()
        return user
Swindell answered 9/11, 2017 at 10:54 Comment(1)
Only the validate_data is not working. The create() and update() are working fine.Neoterize
Z
0

At the time of creating new user(registration) then self.instance will be none, it will work when your are resting the password, change password or updating user data with password. But if you want to check the password should not be similar to your email or username then you need to include "SequenceMatcher" in your validation

data = self.get_initial()
username = data.get("username")
email = data.get("email")
password = data.get("password") 
max_similarity = 0.7
if SequenceMatcher(a=password.lower(), b=username.lower()).quick_ratio() > max_similarity:
    raise serializers.ValidationError("The password is too similar to the username.")
if SequenceMatcher(a=password.lower(), b=email.lower()).quick_ratio() > max_similarity:
    raise serializers.ValidationError("The password is too similar to the email.")
Zibet answered 8/1, 2018 at 5:26 Comment(0)
M
0

validate_password returns None if validation passed and ValidationError if not. There 3 params you can pass:

  1. password
  2. user
By default = None. The user object is optional: if it’s not provided, some validators may not be able to perform any validation and will accept any password.
3. password_validators
By default = None. You can pass your or Django validators with parametr password_validators. If it None, django use default validators.

Read more here: https://docs.djangoproject.com/en/5.0/topics/auth/passwords/#integrating-validation

from django.contrib.auth import password_validation

 def validate(self, data):
    ...
    try:
        password_validation.validate_password(data['password'])
    except Exception as validation_password_error:
        ...
        raise validation_password_error
    ...
    return data
Menon answered 22/1 at 11:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.