Search code examples
djangodjango-authentication

Django: unset a user's password, but still allow password reset


I want to reset/unset the passwords of my users, they should be forced to use the "password reset", and set a new one, that validates with the new password validators.

I found Django docs, so set_unusable_password() is not an option, as the reset is not allowed afterwards. I found that using user.password = '' works - user cannot login, and password reset is working.

Still, this solution feels a bit awkward, and I cant find any real ressources on this topic - how is it security wise, etc.?


Solution

  • You are right, one cannot reset their password after using User.set_unusable_password. From https://docs.djangoproject.com/en/4.0/topics/auth/default/#django.contrib.auth.views.PasswordResetView:

    Users flagged with an unusable password (see set_unusable_password()) aren’t allowed to request a password reset to prevent misuse when using an external authentication source like LDAP.

    Solution 1 - Override the default Django behavior

    Use View and Form classes where you override django.contrib.auth.forms.PasswordResetForm.get_users, which is the Django method that fetches the User instances given an email address. Re-implement it without the if user.has_usable_password condition, so that you can actually leverage the User.set_unusable_password method.

    This way, you stay in the specs that Django is using internally, and the "invalid" passwords can never be matched with an external input.

    Solution 2 - None or empty string

    As you mention, using a None value or an empty string should work. The main downside I see is that, if a code bug/regression is introduced in the authentication logic, it might accept empty/blank passwords and match them against the DB value. And an attacker would get access to these accounts.

    For instance if a user inputs a string containing only characters that are not allowed, and the validation logic accepts it because it is non-empty, and then strips all invalid characters. Or the user inputs a string containing only spaces, which are accepted but later on trimmed. In both cases, it could result in an empty string, which would actually be tested against the user's password, match and connect the user.

    (and of course there are many more solutions which could work :))


    By the way, an interesting note on Django 2.1, see https://docs.djangoproject.com/en/4.0/releases/2.1/#miscellaneous:

    User.has_usable_password() and the is_password_usable() function no longer return False if the password is None or an empty string, or if the password uses a hasher that’s not in the PASSWORD_HASHERS setting. This undocumented behavior was a regression in Django 1.6 and prevented users with such passwords from requesting a password reset. Audit your code to confirm that your usage of these APIs don’t rely on the old behavior.

    So if you are using Django <1.6 or >=2.1, you'll have to work with that.