Atma
Atma

Reputation: 29767

How to check password against previously used passwords in django

I have the following model for storing previously used hashed passwords:

class PasswordHistory(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    password = models.CharField(max_length=128, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now = True)

In the change password form I want check if the new password the user is changing to has not been used the past 5 times.

Here is my form validation:

class ProfileForm(forms.ModelForm):
    password1 = forms.CharField(widget=forms.PasswordInput(), required=False)
    password2 = forms.CharField(widget=forms.PasswordInput(), required=False)

    class Meta:
        model = Employee

        
    user_id = None
    
    def __init__(self, *args, **kwargs):
        self.user_id = kwargs.pop('user_id', None)
        super(ProfileForm, self).__init__(*args, **kwargs)
        
    def clean_password2(self):
        password1 = self.cleaned_data['password1']
        password2 = self.cleaned_data['password2']
        if password1 != password2:
            raise forms.ValidationError('Passwords do not match.')
        

        user = User.objects.get(pk=self.user_id)
        hashed_password = make_password(password1)
        password_histories = PasswordHistory.objects.filter(
        user=user,
        password_hashed_password
    )
    if password_histories.exists():
        raise forms.ValidationError('That password has already been used')        
    return password2

The problem is that the passwords are different every time, even when I attempt the same plain text password over and over again. Therefore:

if password_histories.exists():

Never returns true.

How can I compare past passwords if they are always different due to salt? Thanks

Upvotes: 2

Views: 2736

Answers (1)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 476584

The .set_password function indeed does not return anything, it simply sets the password. Like you say however, the hashing is based on a (random) salt, and thus the hash will be different each time. Therefore you should use the .check_password(…) function [Django-doc], to verify if it somehow matches a hashed variant:

from django.contrib.auth.hashers import check_password

class ProfileForm(forms.ModelForm):
    password1 = forms.CharField(widget=forms.PasswordInput(), required=False)
    password2 = forms.CharField(widget=forms.PasswordInput(), required=False)

    class Meta:
        model = Employee
    
    def __init__(self, *args, **kwargs):
        self.user_id = kwargs.pop('user_id', None)
        super(ProfileForm, self).__init__(*args, **kwargs)
        
    def clean_password2(self):
        password1 = self.cleaned_data['password1']
        password2 = self.cleaned_data['password2']
        if password1 != password2:
            raise forms.ValidationError('Passwords do not match.')
        user = User.objects.get(pk=self.user_id)
        password_histories = PasswordHistory.objects.filter(
            user=user
        )
        for pw in password_histories:
            if check_password(password2, pw.password):
                raise forms.ValidationError('That password has already been used')
        return password2

So if we found a hashed password that matches the given raw password, we can return the password. If by the end of the for loop, we did not find any such password, we can return password2, otherwise we raise an errro.

Upvotes: 4

Related Questions