Can't login in Django project after switching to django 3 and return to django 2

872 views Asked by At

I have a Django 2.2 project that runs in a bunch of different servers, but they use the same database.

I've created a branch to migrate to Django 3, but not all servers will be migrated at the same time.

I use Argon2:

# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers  
PASSWORD_HASHERS = [  
 'django.contrib.auth.hashers.Argon2PasswordHasher',  
 'django.contrib.auth.hashers.PBKDF2PasswordHasher',  
 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',  
 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',  
 'django.contrib.auth.hashers.BCryptPasswordHasher',  
]

When I switched to django 3.2 in my developing branch everything worked fine. But then, when I returned to Django 2.2, I started getting errors like:

  • Template syntax error
  • Incorrect padding (exception location: .../python3.6/base64.py in b64decode)

Those problems where solved by just deleting the cookies and reloading. So I guessed that they were related to the change in django 3.1 to a new default hashing algorithm from sha1 to sha256.

Anyway, after reloading, the page worked. But when I tried to login it didn't recognize the credentials.

Then I restored the database from backup and could login in django 2.2.

I tried again to run on django 3.2 with the setting:

DEFAULT_HASHING_ALGORITHM = 'sha1'

Now, when switching back to 2.2, I didn't get errors on page load (I didn't need to delete cookies) but the credentials still didn't work.

For me it looks like after switching to django 3.2, the hashing of the passwords in the database are changed.

Is it possible that django 3 rewrites passwords in the database? Can anybody point to a solution or something to try?

Thank you.

1

There are 1 answers

0
equalium On

Solution TL;DR

Well, it seems that if you use the Argon2 hasher, Django does indeed update the stored password and if you want to avoid it temporarily you must update to Django 3.1 until ready to move to 3.2 or subclass the hasher.

Comparing stored passwords

Since I can access the production database (django 2.2) and the local one when in Django 3, I compare the passwords on my user:

Django 2.2 (released April 2019)

algorithm: argon2 type: argon2i version: 19 memory cost: 512 time cost: 2 parallelism: 2 salt: UVn ********** hash: QVt *****************

Django 3.2

algorithm: argon2 variety: argon2id version: 19 memory cost: 102,400 time cost: 2 parallelism: 8 salt: pHQzc2 **************** hash: flj ****************

Yes, they are different!

Django 3.2 release notes tell about changes in the default Argon2 hasher:

django.contrib.auth

  • The default iteration count for the PBKDF2 password hasher is increased from 216,000 to 260,000.

  • The default variant for the Argon2 password hasher is changed to Argon2id. memory_cost and parallelism are increased to 102,400 and 8 respectively to match the argon2-cffi defaults.

  • Increasing the memory_cost pushes the required memory from 512 KB to 100 MB. This is still rather conservative but can lead to problems in memory constrained environments. If this is the case, the existing hasher can be subclassed to override the defaults.

  • The default salt entropy for the Argon2, MD5, PBKDF2, SHA-1 password hashers is increased from 71 to 128 bits.

This is the ticket of that change:

#30472 Argon2id should be supported and become the default variety for Argon2PasswordHasher

And looking at the code in django.contrib.auth.hashers, I can see that the passwords are modified on check_password:

def check_password(password, encoded, setter=None, preferred='default'):
    ...
    hasher_changed = hasher.algorithm != preferred.algorithm
    must_update = hasher_changed or preferred.must_update(encoded)
    is_correct = hasher.verify(password, encoded)
    ...

class Argon2PasswordHasher(BasePasswordHasher):
    ...
    def must_update(self, encoded):
        decoded = self.decode(encoded)
        current_params = decoded['params']
        new_params = self.params()
        ...

    def params(self):
        argon2 = self._load_library()
        # salt_len is a noop, because we provide our own salt.
        return argon2.Parameters(
            type=argon2.low_level.Type.ID,
            version=argon2.low_level.ARGON2_VERSION,
            salt_len=argon2.DEFAULT_RANDOM_SALT_LENGTH,
            hash_len=argon2.DEFAULT_HASH_LENGTH,
            time_cost=self.time_cost,
            memory_cost=self.memory_cost,
            parallelism=self.parallelism,
        )

This line type=argon2.low_level.Type.ID changes argon2 type from argon2i to argon2id. The rest of the changes are clear.

I`m not sure if this the real process but I guess it goes something like this:

  • You enter the password
  • The password is checked with the old algorithm
  • If it matches, it is rehashed with the new algorithm and saved

(I'll be glad to know if I'm wrong)

To recap: My problem

My problem is that I have several different Django 2 projects using the same common core code and the same database. Although they are different projects, there are many users who have access to all of them. I want to update them progressively starting with the least sensitive to see if errors arise.

Solution 1

Upgrade to Django 3.1 instead of 3.2. That would allow me to update the different projects progressively without breaking user access along the way. Once all the projects have been running for a while with version 3.1 and fixed any bugs that have emerged, I can update them all at the same time to django 3.2 with greater confidence. This is what I have tested and works (it doesn't change passwords).

Solution 2

Subclass the django.contrib.auth.hashers.Argon2PasswordHasher hasher so it doesn't update passwords. Point to it in the PASSWORD_HASHERS settings and remove it when all projects are running smoothly in django 3.2.